Skip to content
txbrendan/ui

Pricing

beta

A 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

Team accessRequest

The whole team

Teams
every team you run
Members
everyone on the team
Roles
team and admin roles
Set-up
with us, not alone
  • Who's coming, with answer-by dates
  • Reminder emails to whoever hasn't answered
  • A calendar feed for every member
  • Saved agendas and templates
Talk firstFounder

Ask us anything

Who answers
the founder
Based in
Amsterdam
Wants to hear
how your week runs

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

hello@example.com

monthly or yearly, with a saving beside the switch

Save 20% yearly

Starter

One team

$0forever

  • Who's coming, with answer-by dates
  • A calendar feed for every phone
TeamMost picked

Every team

$24a month

  • Everything in Starter
  • Reminder emails
  • Saved agendas
Business

Many teams

$99a month

  • Everything in Team
  • Shared calendars
  • One bill

three plans with prices

Yearly

All-Access

$129a year

  • Every block and template
  • Updates while you subscribe
LifetimeBest value

All-Access

$199once

  • Every block and template
  • Updates forever
  • Unlimited projects
Team

Up to 10

$499once

  • Everything in Lifetime
  • Seats for 10 people

narrow container

Lifetime

All-Access

$199once

Projects
unlimited

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the website component. It also adds every component it uses. Its own files go in your components folder, 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 }

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>