PlanOptionCard

A selectable pricing/plan card, composed inside a RadioGroup, with an optional current-plan indicator and feature list

Installation

pnpm add @wandercom/design-system-web

Usage

PlanOptionCard is not a standalone group — compose it directly inside an existing RadioGroup, the same way you'd place a bare RadioGroupItem:

27 lines
import { PlanOptionCard } from '@wandercom/design-system-web/ui/plan-option-card';
import { RadioGroup } from '@wandercom/design-system-web/ui/radio-group';
import { useState } from 'react';

export default function PlanOptionCardExample() {
  const [plan, setPlan] = useState('small-business');

  return (
    <RadioGroup value={plan} onValueChange={setPlan}>
      <PlanOptionCard
        value="lite"
        name="Wander Lite"
        label="List on wander.com only"
        price="$1/mo"
      />
      <PlanOptionCard
        value="small-business"
        name="Wander Small Business"
        highlightLabel="Recommended"
        label="For managers with 1-9 properties"
        trialLabel="Free trial"
        secondaryPriceLabel="then $39/mo"
        price="$39/mo"
      />
    </RadioGroup>
  );
}

Examples

PlanOptionCard is shared between two surfaces, which differ only in the copy passed to highlightLabel and trialLabel — the component itself doesn't know which surface it's in:

  • Onboarding: highlightLabel="Recommended" on the suggested plan, and every priced plan passes trialLabel="Free trial" with a secondaryPriceLabel (e.g. "then $39/mo") so the price line demotes below it.
  • Dashboard (e.g. the billing "Change plan" screen): highlightLabel="Current plan" on whichever card matches the account's active plan, and prices render plainly with no trialLabel.

Dashboard

A full "Change plan" list: cards with a feature list that animates open only while that card's radio is checked, and a current plan that's grown past its own limit, with a warning linking to the plan it should upgrade to instead.

Loading example...

Onboarding

The same list on the onboarding paywall: a "Recommended" indicator instead of a current-plan one, and every priced plan shows a free trial.

Loading example...

Props

PlanOptionCardProps

value:

string
Value passed through to the underlying RadioGroupItem.

id?:

string
HTML id for the radio input. Defaults to a generated id, so multiple RadioGroups reusing the same values on one page don't collide.

name:

string
Plan name, e.g. "Wander Small Business".

price:

string
Price string, e.g. "$39/mo" or "$—/mo".

label?:

React.ReactNode
Neutral caption, e.g. "For managers with 1-9 properties". Renders alongside `highlightLabel` when both are passed.

highlightLabel?:

React.ReactNode
Accent-colored indicator, e.g. "Recommended" or "Current plan". Renders above `label` when both are passed.

trialLabel?:

React.ReactNode
When passed, renders as the top price line (e.g. "Free trial") and demotes `price` to a secondary line below. Pass `secondaryPriceLabel` for that line's text. Omit `trialLabel` for a plain single-line price.

secondaryPriceLabel?:

React.ReactNode
Text for the secondary price line shown under `trialLabel`, e.g. "then $39/mo". Falls back to `price` alone if `trialLabel` is set but this isn't. Ignored if `trialLabel` isn't passed.

features?:

{ icon: React.ReactNode; title: string; lines: React.ReactNode[] }[]
Feature groups, each with its own icon, title, and description lines rendered one per line. Omit `features` entirely for a compact single-row card with no feature list. When passed, the list is collapsed until this card's radio is checked.

warning?:

React.ReactNode
A note shown as a colored banner in the collapsible body, below the feature list — e.g. a warning that the caller has decided this plan is ineligible (usage exceeds the plan's limit), or an informational aside like a custom-quote nudge — can include links/actions. Never shown in the caption slot next to `label` — `label` always stays put, checked or not.

warningIcon?:

React.ReactNode
Icon shown before `warning` in the expanded banner. Defaults to an alert triangle. Ignored if `warning` isn't passed.

warningColor?:

"error" | "info" | "success" | "warning"
Color used for the `warning` banner's background and icon. Defaults to "warning" (the ineligible-plan treatment). Ignored if `warning` isn't passed.

disabled?:

boolean
Disables the underlying radio input and styles the card as disabled (secondary surface, secondary-colored name/price/caption text). Only the collapsed, single-row layout accounts for this.

className?:

string
Additional CSS classes to apply to the card root.

classNames?:

{ label?, highlightLabel?, price?, trialLabel?, name?, feature?, featureIcon?, featureTitle?, featureDescription?, warning?, warningIcon? }
Granular CSS class overrides for internal elements.

Accessibility

PlanOptionCard composes the native RadioGroup and RadioGroupItem primitives, which follow the WAI-ARIA radio group pattern.

Keyboard interaction:

  • Arrow Up / Arrow Left - Move focus to the previous option
  • Arrow Down / Arrow Right - Move focus to the next option
  • Space - Select the focused option
  • Tab - Move focus into/out of the radio group

Each card is a <label> element with htmlFor linking to its radio input, ensuring screen readers announce the option name when focused.

PlanOptionCard