Skip to Content
BlocksPricingPlansBlock

PricingPlansBlock

Compares plans, switches billing period, and communicates included usage and limits.

Plan idea 55, category Marketing & conversion. Status experimental — see verification.

Import

import { PricingPlansBlock } from "@raydenui/ui/blocks"; import type { PricingPlan, PricingBillingPeriod } from "@raydenui/ui/blocks";

Usage

const periods: PricingBillingPeriod[] = [ { id: "monthly", label: "Monthly" }, { id: "annual", label: "Annual", badge: "Save 20%" }, ]; const plans: PricingPlan[] = [ { id: "team", name: "Team", description: "For a single operations team.", prices: { monthly: { amount: 29, currency: "USD", unit: "per editor / month" }, annual: { amount: 24, currency: "USD", unit: "per editor / month", note: "billed annually" }, }, limits: [{ id: "events", label: "Included events", value: "250,000 / month" }], features: [ { id: "routing", label: "Automatic routing" }, { id: "sso", label: "SAML single sign-on", included: false }, ], cta: { label: "Choose Team", onClick: () => startCheckout("team") }, }, { id: "scale", name: "Scale", badge: "Most popular", highlighted: true, prices: { monthly: { custom: "Custom pricing" }, annual: { custom: "Custom pricing" } }, cta: { label: "Talk to sales", href: "/contact" }, }, ]; <PricingPlansBlock title="Pricing that follows your volume" periods={periods} defaultPeriod="annual" plans={plans} locale="en-US" footnote="Prices exclude local tax." />;

Controlled period

const [period, setPeriod] = useState("monthly"); <PricingPlansBlock title="Plans" periods={periods} period={period} onPeriodChange={setPeriod} plans={plans} />;

The switcher is a fieldset with a legend and a radio group. Supply periodLegend to rename it; it defaults to "Billing period". Fewer than two periods hides the switcher.

Availability

<PricingPlansBlock title="Plans" plans={[ { id: "team", name: "Team", prices: {}, availability: "current", currentLabel: "Your plan" }, { id: "enterprise", name: "Enterprise", prices: {}, availability: "unavailable", unavailableReason: "Available on annual billing only.", }, ]} defaultUnavailableReason="Contact sales to enable this plan." />

An unavailable plan renders an explicitly disabled button whose visible reason is associated through aria-describedby. A current plan renders a non-interactive marker when it supplies no cta.

Never manufacture a success

Choosing a plan only calls cta.onClick or follows cta.href. A confirmation appears only when your application passes notice.

<PricingPlansBlock title="Plans" plans={plans} notice={{ tone: "success", title: "Your plan change is confirmed", description: "Scale takes effect on 1 October.", }} />

Loading, empty and error

<PricingPlansBlock title="Plans" plans={[]} state="loading" loadingPlanCount={3} />
<PricingPlansBlock title="Plans" plans={[]} emptyTitle="No plans published" emptyDescription="Pricing for this region is not available yet." />
<PricingPlansBlock title="Plans" plans={[]} state="error" errorTitle="Pricing could not load" onRetry={() => refetch()} />

Props

PropTypeDefaultDescription
titleReactNode—Required.
plansPricingPlan[]—Required.
eyebrowReactNode—Label above the title.
descriptionReactNode—Supporting explanation.
headingLevel"h1"–"h6""h2"Plan names use the next level down.
periodsPricingBillingPeriod[]—Fewer than two hides the switcher.
periodstring—Controlled period id. Pair with onPeriodChange.
defaultPeriodstringfirst periodUncontrolled initial period id.
onPeriodChange(periodId: string) => void—Period change handler.
periodLegendstring"Billing period"Accessible name for the switcher.
localestring"en-US"BCP 47 tag for currency formatting.
priceFormatOptionsIntl.NumberFormatOptions—Merged into the default format options.
formatPrice(price, context) => ReactNode—Full override of price rendering.
limitsLabelstring"Included usage"Heading above each plan’s limits.
featuresVisibleLimitnumber—Collapses longer feature lists behind a disclosure.
defaultUnavailableReasonstring—Used when a plan supplies none.
noticePricingPlansNotice—A consumer-confirmed outcome. Never set by the block.
footnoteReactNode—Small print under the grid.
state"default" | "loading" | "empty" | "error"derivedResolves to empty for an empty plans array.
loadingPlanCountnumber—Placeholder cards while loading.
loadingLabelstring—Announced while loading.
emptyTitleReactNode—Shown while empty.
emptyDescriptionReactNode—Shown while empty.
errorTitlestring—Shown while state="error".
errorDescriptionReactNode—Shown while state="error".
onRetry() => void—Retry handler. The control is omitted without it.
retryLabelstring—Retry control label.
classNamestring—Lands on the section root.

PricingPlansBlock forwards its ref to the section element and accepts the remaining HTMLAttributes<HTMLElement>.

PricingPlan

MemberTypeDescription
idstringRequired, stable.
nameReactNodeRequired.
descriptionReactNodeOptional.
badgestringCarries the meaning of highlighted in words.
highlightedbooleanVisual emphasis. Always pair with badge so it is not colour-only.
pricesRecord<string, PricingPrice>Required. Keyed by PricingBillingPeriod.id.
limitsPricingLimit[]{ id, label, value }, pre-formatted.
featuresPricingFeature[]{ id, label, included?, note? }.
ctaPricingCtaRequired for an available plan; omit for current.
availability"available" | "current" | "unavailable"Defaults to "available".
unavailableReasonstringAnnounced through aria-describedby.
currentLabelstringReplaces the default “Current plan” marker.

PricingPrice

MemberTypeDescription
amountnumberAmount in the currency’s major unit, unlike the commerce blocks.
currencystringISO 4217 code. Required alongside amount.
unitReactNodeRendered after the amount, e.g. “per editor / month”.
customReactNodeReplaces the amount entirely. Takes precedence.
originalAmountnumberReference amount shown struck through.
noteReactNodeSmall print under the price.

States

  • default — the plan grid at the selected period.
  • loading — placeholder cards with a status message.
  • empty — no plans published.
  • error — role="alert" with an optional retry.
  • Per plan — available, current, unavailable.
  • No success state is manufactured. A confirmation requires notice.

Excluded features carry a visually hidden “Not included:” prefix as well as a distinct icon, so the distinction never rests on colour.

Verification

Measured 22 September 2026 with Playwright 1.58.2 driving installed Google Chrome and axe-core 4.11.1, at 320, 390, 768 and 1440 CSS pixels in light and dark mode. Interaction checks covered clicking and arrow-keying the period switcher, a 147Ă—38px switch target, the feature disclosure expanding 14 to 23 items, and an aria-describedby on unavailable plans resolving to a real element. Re-run in the repository story suite on 23 September 2026.

Not verified: a screen-reader pass, any non-Chromium engine, or operating-system text-size and full-browser-zoom settings. Target size was measured for the period switcher only.

Known limitations

  • Prices are amounts in the currency’s major unit. The commerce blocks use minor units. The two families do not share a money convention.
  • The notice panel is rendered locally rather than with Alert, which hard-codes bg-white and would not follow a consumer --color-surface override.
  • An earlier draft dimmed unavailable plan cards with opacity-90, which measured every label in that card at 4.03:1 against white. The dimming was removed; unavailability is now carried by the disabled control and its associated reason.
Last updated on