Pricing
betaA row of plan cards, light and dark, with mono labels, label and value rows, what is included, and a call to action.
Built from Choice Chips, Morph Label, and Rolling Number.
Preview
The whole team
- Who's coming, with answer-by dates
- Reminder emails to whoever hasn't answered
- A calendar feed for every member
- Saved agendas and templates
Ask us anything
Every team I've worked on was disorganized in roughly the same way. If yours is too, tell me how the week runs, and we'll set it up together.
- Your team's name and size
- Where the schedule lives today
- What goes wrong most weeks
monthly or yearly, with a saving beside the switch
One team
$0forever
- Who's coming, with answer-by dates
- A calendar feed for every phone
Every team
$24a month
- Everything in Starter
- Reminder emails
- Saved agendas
Many teams
$99a month
- Everything in Team
- Shared calendars
- One bill
three plans with prices
All-Access
$129a year
- Every block and template
- Updates while you subscribe
All-Access
$199once
- Every block and template
- Updates forever
- Unlimited projects
Up to 10
$499once
- Everything in Lifetime
- Seats for 10 people
narrow container
All-Access
$199once
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the website component. It also adds every component it uses. Its own files go in your
componentsfolder, and they are yours to edit.npx shadcn@latest add @txbrendan/pricing
Use it for
- A pricing page, or the closing section of a landing page, under a Headline.
- Two to four plans. Make the one you want people to pick solid, the dark card, and the rest soft.
- Use `details` for short facts, like seats or projects, and `features` for what is included.
- Put one Button in each card's `action`. It fills the card's width.
- A contact card works too: leave out the price, and add a `description` and a `footnote` with your email.
- For monthly and yearly prices, pass `billing` to Pricing, and give each card's `price` and `period` one entry per period id. A plain string stays the same on every period.
- Put a saving, like "Save 20% yearly", in `billing.note`, or in the yearly `period`, like "a month, billed yearly". Keep the switch's labels to the period's name.
- The headline is set in the theme's `font-headline`. Load Geist Serif as `--font-geist-serif`, like `geist/font/serif/variable` in Next.js, or it falls back to the system serif.
Avoid it for
- More than four plans. People stop comparing past that. Link to a full table, or use Comparison.
- Prices that change by region or seat count in the card itself. Show the starting price, and explain the rest.
- A plan comparison of many features. Use Comparison.
- A billing switch with more than three periods, or with one. Show a single period as a plain price.
Props
- eyebrow
PricingCard. A small mono label at the top left.
string
- tag
PricingCard. A small mono label at the top right.
string
- title
PricingCard. The plan's name, large. It is an h3.
ReactNode
- price
PricingCard. The price, if you show one. Under a billing switch, one entry per period id: { monthly: "$24", yearly: "$240" }.
string | Record<string, string>
- period
PricingCard. What the price covers, after it: once, a year. Under a billing switch, it can be one entry per period id too.
string | Record<string, string>
- details
PricingCard. Short facts as rows under a dashed line, values in the brand color. Labels must be unique.
{ label: string; value: string }[]
- description
PricingCard. A paragraph, like why the plan exists.
ReactNode
- features
PricingCard. What is included, one line each, with a plus. Each must be unique.
string[]
- action
PricingCard. The call to action, like a Button. It fills the card's width.
ReactNode
- footnote
PricingCard. A small line under the action.
ReactNode
- emphasis
PricingCard. solid is a dark card that stands out, in both modes. soft is a light gray one.
"solid" | "soft" · default "solid"
- className
Extra classes on the root. On Pricing it holds the container query.
string
- Pricing
Part. The row: cards stack when narrow, and sit side by side at equal widths from 42rem of its own width.
ComponentProps<"div">
- billing
Pricing. A switch above the cards between billing periods. It starts on defaultValue, or the first option. label names it for screen readers, default "Billing". note is a short line beside it. announce returns what screen readers hear after a change, default "Showing yearly prices" from the option's label; pass it for another language.
{ options: { id: string; label: string }[]; defaultValue?: string; value?: string; onValueChange?: (value: string) => void; label?: string; note?: ReactNode; announce?: (option: { id: string; label: string }) => string }
| Prop | Type | Default | What it does |
|---|---|---|---|
| eyebrow | string | - | PricingCard. A small mono label at the top left. |
| tag | string | - | PricingCard. A small mono label at the top right. |
| title | ReactNode | - | PricingCard. The plan's name, large. It is an h3. |
| price | string | Record<string, string> | - | PricingCard. The price, if you show one. Under a billing switch, one entry per period id: { monthly: "$24", yearly: "$240" }. |
| period | string | Record<string, string> | - | PricingCard. What the price covers, after it: once, a year. Under a billing switch, it can be one entry per period id too. |
| details | { label: string; value: string }[] | - | PricingCard. Short facts as rows under a dashed line, values in the brand color. Labels must be unique. |
| description | ReactNode | - | PricingCard. A paragraph, like why the plan exists. |
| features | string[] | - | PricingCard. What is included, one line each, with a plus. Each must be unique. |
| action | ReactNode | - | PricingCard. The call to action, like a Button. It fills the card's width. |
| footnote | ReactNode | - | PricingCard. A small line under the action. |
| emphasis | "solid" | "soft" | "solid" | PricingCard. solid is a dark card that stands out, in both modes. soft is a light gray one. |
| className | string | - | Extra classes on the root. On Pricing it holds the container query. |
| Pricing | ComponentProps<"div"> | - | Part. The row: cards stack when narrow, and sit side by side at equal widths from 42rem of its own width. |
| billing | { options: { id: string; label: string }[]; defaultValue?: string; value?: string; onValueChange?: (value: string) => void; label?: string; note?: ReactNode; announce?: (option: { id: string; label: string }) => string } | - | Pricing. A switch above the cards between billing periods. It starts on defaultValue, or the first option. label names it for screen readers, default "Billing". note is a short line beside it. announce returns what screen readers hear after a change, default "Showing yearly prices" from the option's label; pass it for another language. |
Accessibility
- Each plan name is an h3, so put the row under an h2, like a Headline.
- Details are a description list, so screen readers pair each label with its value.
- A solid card uses the theme's own dark colors, so every pair in it is one the theme checks in dark mode.
- In forced colors a soft card gets a border, so its edge still shows when its fill is painted away.
- Brand values on the soft card use tone text on muted, which the theme checks at 4.5:1.
- Plus marks are aria-hidden. Each feature reads as a plain list item.
- The billing switch is a radio group named by its label, so screen readers say which period is on. The arrow keys move between periods.
- A period is always picked: pressing the picked one keeps it.
- The prices change in place. A hidden polite status beside the switch then says so, like "Showing yearly prices". It is silent on page load, and speaks after a press, an arrow key, or a controlled value change.
- The card keeps room for the longest period, so it does not jump when the period changes. The other periods' prices are hidden from screen readers.
Examples
Two plans
<Pricing>
<PricingCard emphasis="soft" eyebrow="Yearly" title="All-Access" price="$129" period="a year"
features={["Every block and template"]} action={<Button emphasis="outline">Choose yearly</Button>} />
<PricingCard eyebrow="Lifetime" tag="Best value" title="All-Access" price="$199" period="once"
features={["Every block and template", "Updates forever"]} action={<Button tone="brand">Choose lifetime</Button>} />
</Pricing>Monthly or yearly
<Pricing
billing={{
options: [{ id: "monthly", label: "Monthly" }, { id: "yearly", label: "Yearly" }],
note: "Save 20% yearly",
}}
>
<PricingCard emphasis="soft" title="Starter" price="$0" period="forever" action={<Button emphasis="outline">Start free</Button>} />
<PricingCard
title="Team"
price={{ monthly: "$24", yearly: "$19" }}
period={{ monthly: "a month", yearly: "a month, billed yearly" }}
action={<Button tone="brand">Start a trial</Button>}
/>
</Pricing>