Plan Card
The current subscription plan: name, price and interval, status with the right text, renewal or end date, feature list, a usage hint, and the actions Change plan and Cancel subscription (confirmed, with the end date). A past due plan shows an alert. PlanPicker, exported beside it, lets people choose another plan. 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
Statuses
States
Permissions
ClassNames
Plan picker
This is your current plan.
Billing page
Billing
Invoices
6 results
Showing 1 to 5 of 6
Installation
npx rdloom add plan-cardCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/plan-card.jsonWorks, but without upgrade tracking.
Usage
import { PlanCard } from "@rdloom/react";
<div className="flex w-full justify-center">
<div className="w-full max-w-md">
<PlanCard
plan={{
name: "Team plan",
price: 49,
interval: "month",
features: ["Up to 10 seats", "5 GB of storage", "Email support"],
}}
periodEnd="2027-03-03"
usageHint="8 of 10 seats used"
onChangePlan={() => wait(400)}
onCancel={() => wait(600)}
/>
</div>
</div>API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
planThe plan: its name, the price per interval (in whole units, e.g. 49 for $49), an ISO currency code (default USD), an optional description and the features it includes. | { name: string; price: number; interval: "month" | "year"; currency?: string; description?: string; features?: string[] } | none |
statusWhere the subscription stands. Each status has its own badge text and date line: active "Renews on", trialing "Trial ends on", past due "Payment failed", canceled "Ends on". | "active" | "trialing" | "past_due" | "canceled" | "active" |
periodEndWhen the current period ends: the renewal date, the trial end or the end of access. An ISO date string or a Date. | Date | string | none |
usageHintOne line about use, e.g. "8 of 10 seats used". For a meter use UsageMeter beside the card. | string | none |
localeThe locale for the price and the dates. | string | "en-US" |
stateThe data state: loading shows a skeleton, empty says there is no plan yet and offers Choose a plan, error shows a message with Try again. | DataState | none |
onRetryAdds a Try again button to the error state. | () => void | none |
onChangePlanStarts changing the plan; yours, async. Usually opens a PlanPicker. | () => void | Promise<unknown> | none |
onCancelCancels the subscription after the person confirmed in the dialog, which states the end date; yours, async. | () => void | Promise<unknown> | none |
onUpdatePaymentAdds an Update payment method button to the past due alert. | () => void | Promise<unknown> | none |
permissionsWhat the app allows: changePlan and cancel. 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<"changePlan" | "cancel"> | none |
classNamesExtra class names for single parts, so you can restyle one part without editing the file. Keys: root, header, name, status, price, renewal, features, feature, usage, alert, actions, changeButton, cancelButton, updateButton. | Partial<Record<"root" | "header" | "name" | "status" | "price" | "renewal" | "features" | "feature" | "usage" | "alert" | "actions" | "changeButton" | "cancelButton" | "updateButton", 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 Change plan and Cancel subscription
- Cancel subscription opens a confirmation: Escape cancels, Enter confirms
Screen readers announce
- "Team plan, group"
- "Past due"
- "Renews on 3 March 2027"
- "Cancel subscription, button"; the dialog says "You keep access until 3 March 2027"
What your code must do
- The card is a group named by the plan name
- The status is a badge with words (Active, Trial, Past due, Canceled), never only a color
- A past due plan shows an alert that says what to do
- The features are a list; each tick is decorative
- The cancel confirmation states the date access ends
- A disabled action stays focusable and the reason is its description
Block contract
- Data
- The current plan, its status and period end, and async callbacks for changing, cancelling and fixing payment.
- Data states
- loading, empty, error, ready
- Permissions
- changePlan, cancel
- Events
- onChangePlan, onCancel, onUpdatePayment, onRetry
- You can replace
- classNames for each part; status and periodEnd; usageHint and locale; PlanPicker for choosing a plan
Guidelines
Use it when
- Showing the plan a workspace is on with its renewal and the way to change or end it
- A billing page next to UsageMeter and PaymentMethodCard
Avoid it when
- Comparing plans before choosing: use PlanPicker
- Listing invoices: use DataTable
Don't
- Treating the permissions prop as protection: UI permission is not security, so the server must check again
- Cancelling from inside the component: onCancel is yours, and so is the end date it shows
- Showing the status by color alone: the badge text carries it
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-surface-default--rd-color-border-default--rd-color-text-default--rd-color-text-muted--rd-color-action-primary--rd-color-feedback-success--rd-color-feedback-warning--rd-color-feedback-danger--rd-elevation-raised--rd-radius-overlay