Payment Method Card
One payment method: a generic card mark, the last four digits, the expiry with an expires soon notice, a default badge, and the actions Update, Remove (confirmed) and Make default. With no method it offers Add a payment method. It never holds a full card number; your callbacks do the work. UI permission is not security: the server must check again.
A ready-made piece, built from the library's own parts.
A block puts several components together into something you would otherwise assemble by hand. It never fetches data: you give it the data, or answer its callbacks. It is copied into your project like any component, with the parts it uses, so you can change anything.
Basic
Expiring
Empty and states
Permissions
ClassNames
Installation
npx rdloom add payment-method-cardCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/payment-method-card.jsonWorks, but without upgrade tracking.
Usage
import { PaymentMethodCard } from "@rdloom/react";
<div className="flex w-full justify-center">
<div className="w-full max-w-md">
<PaymentMethodCard
method={method}
onUpdate={() => wait(400)}
onMakeDefault={() => wait(400)}
onRemove={async () => {
await wait(500);
setMethod(undefined);
}}
onAdd={async () => {
await wait(400);
setMethod(card);
}}
/>
</div>
</div>API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
methodThe method to show. brand is a short word for the chip (e.g. "Credit"); only the last four digits are ever shown. Leave out for the empty state. | { id: string; brand?: string; last4: string; expMonth: number; expYear: number; holder?: string; isDefault?: boolean } | none |
expiresSoonDaysHow many days before the end of the expiry month the card counts as expiring soon. | number | 60 |
nowThe current date for the expiry check. Defaults to today; pass one for a fixed result in tests and server rendering. | Date | none |
stateThe data state: loading shows a skeleton, empty (or no method) offers Add a payment method, error shows a message with Try again. | DataState | none |
onRetryAdds a Try again button to the error state. | () => void | none |
onAddStarts adding a method from the empty state; yours, async. | () => void | Promise<unknown> | none |
onUpdateStarts updating the method (usually opens your payment form); yours, async. | (method: { id: string; brand?: string; last4: string; expMonth: number; expYear: number; holder?: string; isDefault?: boolean }) => void | Promise<unknown> | none |
onRemoveRemoves the method after the person confirmed; yours, async. | (method: { id: string; brand?: string; last4: string; expMonth: number; expYear: number; holder?: string; isDefault?: boolean }) => void | Promise<unknown> | none |
onMakeDefaultMakes this the default method; yours, async. The button is not shown on the default method. | (method: { id: string; brand?: string; last4: string; expMonth: number; expYear: number; holder?: string; isDefault?: boolean }) => void | Promise<unknown> | none |
permissionsWhat the app allows: add, update, remove and makeDefault. Hidden renders nothing; disabled keeps the control reachable (aria-disabled) with the reason shown and read, and nothing runs. This only changes what people see: the server must check again. | Permissions<"add" | "update" | "remove" | "makeDefault"> | none |
classNamesExtra class names for single parts, so you can restyle one part without editing the file. Keys: root, brand, details, number, expiry, badge, notice, actions, updateButton, removeButton, defaultButton, addButton, status. | Partial<Record<"root" | "brand" | "details" | "number" | "expiry" | "badge" | "notice" | "actions" | "updateButton" | "removeButton" | "defaultButton" | "addButton" | "status", string>> | none |
Accessibility
Role group, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab: moves through Update, Make default and Remove
- Remove opens a confirmation: Escape cancels
Screen readers announce
- "Payment method, ending in 4242, group"
- "Default"
- "Expires soon, 08/2026"
- "Payment method removed"
What your code must do
- The card is a group named "Payment method" followed by the last four digits
- The brand mark is decorative; the digits are said as text, e.g. "ending in 4242"
- Expires soon and Expired are words with an icon, never only a color
- Default is a badge with text
- Removing needs a confirmation and is announced when done
- After a removal focus moves to the card so it is not lost
- A disabled action stays focusable and the reason is its description
Block contract
- Data
- One payment method (brand word, last four digits, expiry, default flag) and async callbacks to add, update, remove and make default.
- Data states
- loading, empty, error, ready
- Permissions
- add, update, remove, makeDefault
- Events
- onAdd, onUpdate, onRemove, onMakeDefault, onRetry
- You can replace
- classNames for each part; expiresSoonDays; brand word per method; now for a fixed date
Guidelines
Use it when
- Showing the card or method a workspace pays with on a billing page
- A list of methods: render one PaymentMethodCard each
Avoid it when
- Collecting card details: use your payment provider's own fields, never your own inputs
Don't
- Treating the permissions prop as protection: UI permission is not security, so the server must check again
- Passing a full card number: only the last four digits belong in the browser
- Showing a card network logo you do not have the right to: the brand chip is plain text on purpose
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-surface-default--rd-color-surface-subtle--rd-color-border-default--rd-color-text-default--rd-color-text-muted--rd-color-feedback-warning--rd-color-feedback-danger--rd-elevation-raised--rd-radius-overlay--rd-radius-control