Skip to content

PlanCard

A priced plan choice: a radio drawn as a card with the price, how it is billed, a detail line and a strong badge, as a full-width row or a tile.

  • Pro, mobile
  • Version 2.0.0-rc.1
  • APG pattern: Radio group

Examples

Still previews of the real component, rendered from its stories. Pro code isn’t public, so nothing in them responds. Switch the theme and density for every preview at once.

Default

Choose a plan
Show the code

Default, code

<RadioGroup label="Choose a plan" defaultValue="annual">
  <PlanCard value="annual" name="Annual" price="£59.99" billed="Billed yearly" detail="Just £5.00 a month" badge="Save 50%" />
  <PlanCard value="monthly" name="Monthly" price="£9.99" billed="Billed monthly" detail="Cancel any time" />
</RadioGroup>

Tiles

Choose a plan
Show the code

Tiles, code

<RadioGroup label="Choose a plan" defaultValue="annual" orientation="horizontal">
  <PlanCard layout="tile" value="annual" name="Annual" price="£59.99" billed="Billed yearly" badge="Save 50%" />
  <PlanCard layout="tile" value="monthly" name="Monthly" price="£9.99" billed="Billed monthly" />
</RadioGroup>

Loading

Choose a plan
Show the code

Loading, code

<RadioGroup label="Choose a plan">
  <PlanCard value="annual" name="Annual" price="£59.99" billed="Billed yearly" isLoading />
  <PlanCard value="monthly" name="Monthly" price="£9.99" billed="Billed monthly" isDisabled detail="Not available in your region" />
</RadioGroup>

Tokens

The 26 design tokens PlanCard reads, with the value each has in the theme and density chosen above. Change a token and the component follows, in Figma and in code.

Show all 26 tokens
Tokens PlanCard reads
TokenValue nowCSS variable
checkbox.bg…--sd-checkbox-bg
checkbox.bg-checked…--sd-checkbox-bg-checked
checkbox.border…--sd-checkbox-border
checkbox.border-width…--sd-checkbox-border-width
color.bg.brand-subtle…--sd-color-bg-brand-subtle
color.bg.brand-subtle-hover…--sd-color-bg-brand-subtle-hover
color.bg.canvas…--sd-color-bg-canvas
color.bg.muted…--sd-color-bg-muted
color.bg.subtle…--sd-color-bg-subtle
color.border.brand…--sd-color-border-brand
color.border.control…--sd-color-border-control
color.border.subtle…--sd-color-border-subtle
color.icon.default…--sd-color-icon-default
color.text.default…--sd-color-text-default
color.text.disabled…--sd-color-text-disabled
color.text.on-primary…--sd-color-text-on-primary
color.text.secondary…--sd-color-text-secondary
control.indicator…--sd-control-indicator
motion.duration.fast…--sd-motion-duration-fast
motion.easing.standard…--sd-motion-easing-standard
radius.full…--sd-radius-full
shape.radius-surface…--sd-shape-radius-surface
spacing.gap.md…--sd-spacing-gap-md
spacing.gap.sm…--sd-spacing-gap-sm
spacing.gap.xs…--sd-spacing-gap-xs
spacing.inset.md…--sd-spacing-inset-md

Install

Pro components come as source from the SuperDuper registry, with a licence key from Solo, Team or Studio. The registry and checkout aren’t open yet.

Add it from the registry (Pro licence key)

npx shadcn@latest add @superduperui/plan-card

Then import it

import { PlanCard } from "@/components/ui/plan-card"

Props

PlanCard props
PropTypeWhat it does
valuerequiredstringThis plan's value in its RadioGroup.
namerequiredstringThe plan's name ("Annual").
pricerequiredstringThe price as shown ("£59.99").
billedstringHow it is billed ("Billed yearly"). Read after the price.
detailstringOne line under the name ("Just £5.00 a month"), read as the description.
badgestringA short callout ("Save 50%"), drawn as a strong Badge and read last in the name.
layout'row' | 'tile'Default 'row'row (default): one full-width row. tile: a card for two or three side by side.
isLoadingbooleanDefault falseThe plan is still loading (a price or a trial check): a spinner replaces the radio, aria-busy is set and presses are ignored.
isDisabledboolean
classNamestring

When to use it

Use it for

  • Choosing a subscription plan on a paywall
  • Two or three plans side by side (tiles) on a phone

Use something else for

  • Comparing many features: use a Table
  • Choices without a price: use RadioCard or ChoiceRow

Accessibility

Screen readers announce "Annual £59.99 Billed yearly Save 50%, Just £5.00 a month, radio button, checked, 1 of 2".

WCAG 2.2 AAA criteria the component meets in Comfortable density: 1.4.6, 1.4.9, 2.1.3, 2.3.2, 2.4.13, 2.5.5.

Rows stay at least 72px: density changes spacing only.

What your product still has to do

  • Put the cards in a RadioGroup, with the question ("Choose a plan") as the group label
  • Show the full price and how it is billed on the card, not only a monthly equivalent

This release’s test results

Test results for PlanCard in this release
axe-core (WCAG A, AA, AAA and best practice)80 runs across every story, theme, density and width: 0 violations
Rendered text contrast345 text runs: 0 below 7:1, lowest 9.8:1
Target size28 targets: 0 under 44px in Comfortable
Focus appearance (2.4.13)18 focus stops in 3 browser engines: all pass
Keyboard scriptChromium 3 of 3, Firefox 3 of 3, WebKit 3 of 3

Manual passes with VoiceOver and NVDA haven’t been done yet.

Keyboard

Each step is scripted and run in Chromium, Firefox and WebKit.

PlanCard keyboard steps
KeyWhat happens
TabTab enters the group on the selected plan
ArrowDownDown Arrow moves to the next plan and selects it
ArrowUpUp Arrow moves back

In Figma

The Figma component is PlanCard, with Selected (False, True); State (Default, Pressed, Focus, Loading); Layout (Row, Tile). The Figma library isn’t published yet.

Figma to code: Selected (False, True = RadioGroup value (the group value holds this card's value)); State (Default, Pressed, Focus, Loading = isLoading); Layout → layout (Row = "row", Tile = "tile"); Name → name; Price → price; Billed → billed; Detail → detail when Show detail; Badge → badge when Show badge.