Plan picker

Pro

Choose 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.

Markdown

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

TSX · The example above
"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>

PropTypeDefaultDescription
plansrequiredPlanTier[]—The plans to choose from, one card each.
currentPlanTier["id"] | undefined—The account's plan, marked Current and selected at first. Undefined means no plan yet.
seatsnumber | undefined1The starting seat count. Defaults to 1, and never below minSeats.
minSeatsnumber | undefined1Seats can't go below members already in the account.
termsstring | 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.
classNamestring | 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.

KeyEnglish
labelPlans
seatsSeats
fewerSeatsFewer seats
moreSeatsMore seats
atLeastAt least {count}: one per member.
planPlan
recommendedRecommended
currentCurrent
price${amount}
perSeatMonth/ seat / month
monthFor{ one: "a month for {count} seat", other: "a month for {count} seats" }
updateSeatsUpdate seats
chooseChoose {plan}
chooseAPlanChoose a plan