Plan picker
ProChoose a plan and seats. Prices are per seat and flat: the total is the per-seat price times seats, shown before anything is charged. The current plan is marked; the recommended one is called out but not pre-selected. What every plan includes, and how billing works, is the app's to say (terms), once, plainly, under the cards.
Preview
Install
With the Candor CLI
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/plan-picker
Brings in: button, locale, ops, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import { PlanPicker } from "@/components/plan-picker"
import type { PlanTier } from "@/lib/ops"
const plans: PlanTier[] = [
{ id: "team", name: "Team", perSeat: 249, tagline: "The whole product for an in-house team", includes: ["Chat, agents and reports", "Approvals and audit log", "SSO sign-in"] },
{ id: "enterprise", name: "Enterprise", perSeat: 499, tagline: "Everything in Team, plus people behind it", includes: ["Everything in Team", "Security and deployment controls", "A named engineer on your account"], recommended: true },
{ id: "consultancy", name: "Agency", perSeat: 599, tagline: "For agencies running many clients’ apps", includes: ["Everything in Enterprise", "Clients’ apps under one account", "Per-client separation"] },
]
export default function Example() {
return <PlanPicker className="max-w-4xl" plans={plans} current="team" seats={6} minSeats={6} terms="Prices are per seat, billed monthly. Changes take effect on your next invoice." onChoose={() => {}} />
}<PlanPicker>
| Prop | Type | Default | Description |
|---|---|---|---|
| plansrequired | PlanTier[] | — | The plans to choose from, one card each. |
| current | PlanTier["id"] | undefined | — | The account's plan, marked Current and selected at first. Undefined means no plan yet. |
| seats | number | undefined | 1 | The starting seat count. Defaults to 1, and never below minSeats. |
| minSeats | number | undefined | 1 | Seats can't go below members already in the account. |
| terms | string | undefined | — | What every plan includes and how billing works, e.g. "No trial: billing starts when you choose a plan." |
| onChooserequired | (plan: PlanTier["id"], seats: number) => void | — | Called with the chosen plan and seat count; nothing is charged before this. |
| className | string | undefined | — | Extra classes for the outer element. |
Strings
The fixed words, in English by default. Change any of them under planPicker in <LocaleProvider strings={{ … }}>; the table is exported as planPickerStrings.
| Key | English |
|---|---|
| label | Plans |
| seats | Seats |
| fewerSeats | Fewer seats |
| moreSeats | More seats |
| atLeast | At least {count}: one per member. |
| plan | Plan |
| recommended | Recommended |
| current | Current |
| price | ${amount} |
| perSeatMonth | / seat / month |
| monthFor | { one: "a month for {count} seat", other: "a month for {count} seats" } |
| updateSeats | Update seats |
| choose | Choose {plan} |
| chooseAPlan | Choose a plan |