Pricing Table

( Pricing / Landing / Saas )

A three-column pricing section with an animated monthly/yearly toggle and per-feature comparison.

Live preview and source code for Pricing Table

Simple, honest pricing

Start free. Upgrade when your work outgrows it — cancel any time.

Starter

Everything you need to publish your first project.

$0forever
No card required
  • 3 projects
  • Community support
  • Basic analytics
  • Custom domain
  • Priority review

Studio

Scale across clients with shared workspaces and SSO.

$79/mo
billed monthly
  • Everything in Pro
  • Dedicated manager
  • Usage reporting
  • SSO & audit log
  • Priority review

A three-column pricing section with a monthly and yearly billing toggle, a featured middle tier and a check or dash against every feature. The toggle slides a pill between the two options and the price itself animates when the cycle flips, so the change is visible without the layout moving. Tiers, the currency symbol and the starting cycle are props, and the defaults are a complete example you can read before replacing.

Installation

Copy the source from the Code tab above into your project as PricingTable.tsx. Its styles are a <style> block of pt- prefixed classes that travels with the file, so there is no stylesheet to import.

It imports framer-motion for the toggle pill and the price transition, and lucide-react for the check and dash icons:

Terminal
npm install framer-motion lucide-react

It also renders two items from this collection, so copy those files too and fix the import paths at the top to match where you put them: Badge and Button.

Usage

Rendered with no props it shows three example tiers. Pass your own to replace them:

Example.tsx
import PricingTable from "./components/PricingTable";

const tiers = [
    {
        name: "Starter",
        monthly: 0,
        yearly: 0,
        description: "Everything you need to publish your first project.",
        cta: "Get started",
        features: [
            { label: "3 projects", included: true },
            { label: "Custom domain", included: false },
        ],
    },
];

export default function Example() {
    return <PricingTable tiers={tiers} currency="$" defaultCycle="monthly" />;
}

monthly and yearly are both per-month numbers, where yearly is the discounted rate when billed annually. featured on a tier lifts the card and adds the "Most popular" ribbon. yearlyNote is the discount label beside the toggle, and an empty string removes it.

When to reach for it

A SaaS pricing page with a small number of plans and an annual discount, which is what the toggle and the per-month yearly figure are shaped for.

A comparison section where each plan's features matter more than a full matrix: five or six rows per tier, marked included or not.

An upgrade screen inside a product, using defaultCycle="yearly" so the discounted price is what a returning user sees first.

When not to

If you have more or fewer than three tiers. The grid is a fixed three columns above 900px. Two tiers leave an empty column and four wrap into a row of three plus one on its own.

If pricing is usage based or quoted. There is one number per tier per cycle. Sliders, seat counts, metered rates and "contact us" ranges all need a different layout.

If prices need real formatting. Amounts are printed as raw numbers behind a symbol, so there are no thousands separators, no decimals and no locale awareness. Anything beyond a whole dollar or euro means formatting the value before it reaches the tier.

Accessibility

The toggle is a pair of real buttons in a role="group" labelled "Billing cycle", and each carries aria-pressed, so the current cycle is announced rather than only shown by the sliding pill.

Included and excluded features are marked with a check and a dash rather than with colour alone, but neither icon has a text alternative, so a screen reader reads the feature label with nothing to say whether it is included. If that matters, add visually hidden text beside each icon in the copied file.

None of the animation is gated on prefers-reduced-motion: the pill slides, the price blurs and moves, and the discount badge animates in and out.

Notes

The section heading and its subtitle are hard-coded in the source rather than being props. They are the first thing to change in the copied file.

Feature lists belong to each tier independently, so nothing aligns rows across columns. Keeping the labels in the same order in every tier is what makes the table read as a comparison, and the component will not do it for you.

A tier priced at 0 prints "forever" instead of "/mo" and "No card required" instead of the billing line, in both cycles.

The call to action on each card is a Button with no click handler. Wire each one to your checkout before shipping.