Pricing table
Two to four plans side by side with one emphasised, in a calm monochrome.
npx shadcn@latest add https://ui.goodtabs.co/r/pricing-table.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/pricing-table
Simple pricing
Start free. Upgrade when the team grows.
Free
For side projects.
$0/month
- 3 projects
- Community support
- 7 day history
Pro
PopularFor small teams.
$12/month
- Unlimited projects
- Priority support
- 1 year history
Free and Pro, with Pro emphasised.
Three plans
Pick a plan
Hobby
Try it on a personal project.
$0/month
- 1 workspace
- 5 components a day
- Community support
Team
PopularShared libraries for a product team.
$18/seat / month
- Unlimited workspaces
- Shared collections
- Audit log
- Priority support
Studio
Many clients, one account.
$49/month
- Everything in Team
- Client workspaces
- Custom domains
- Invoice billing
Three columns from md, one plan emphasised.
Usage
import { PricingTable } from "@/components/pricing-table"
<PricingTable plans={[{ name: "Free", price: "$0", features: ["3 projects"] }, { name: "Pro", price: "$12", period: "month", emphasised: true, features: ["Unlimited projects"], action: <Button>Get Pro</Button> }]} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| plans | Plan[] | name, price, features and optional description, period, action and emphasised. | |
| title | string | Section heading. | |
| description | string | One line under the heading. |
Source
pricing-table/pricing-table.tsx
import * as React from "react";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
type Plan = {
/** Plan name, for example "Pro". */
name: string;
/** One line on who it is for. */
description?: string;
/** Formatted price, for example "$12" or "Free". */
price: string;
/** Billing period drawn as mono "/month". Leave it out for none. */
period?: string;
/** Included items, one short line each. */
features: string[];
/** The plan's action, usually a Button. */
action?: React.ReactNode;
/** Emphasise this plan. Use on one plan only. */
emphasised?: boolean;
};
type PricingTableProps = Omit<React.ComponentProps<"section">, "title"> & {
/** Section heading. */
title?: string;
/** One line under the heading. */
description?: string;
/** Two to four plans, side by side from md up. */
plans: Plan[];
};
const COLS: Record<number, string> = {
1: "md:grid-cols-1",
2: "md:grid-cols-2",
3: "md:grid-cols-3",
4: "md:grid-cols-2 lg:grid-cols-4",
};
/**
* Plans side by side as one hairline-divided table of columns. The emphasised plan gets a
* foreground ring, a Popular mark and a lifted surface; there is no colour. Server component.
*/
export function PricingTable({ title, description, plans, className, ...props }: PricingTableProps) {
return (
<section data-slot="pricing-table" className={cn("flex w-full flex-col gap-6", className)} {...props}>
{(title || description) && (
<header className="flex flex-col gap-1.5">
{title && <h2 className="m-0 text-2xl leading-tight font-medium tracking-tight text-balance">{title}</h2>}
{description && <p className="m-0 text-sm text-muted-foreground">{description}</p>}
</header>
)}
<ul
className={cn(
"m-0 grid list-none grid-cols-1 gap-3 p-0 [overflow-wrap:anywhere]",
COLS[Math.min(plans.length, 4)],
)}
>
{plans.map((plan, i) => (
<li
key={i}
data-emphasised={plan.emphasised || undefined}
className={cn(
"surface-shine flex flex-col gap-5 rounded-lg border bg-card p-5",
plan.emphasised && "border-foreground/40 ring-1 ring-foreground/40",
)}
>
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between gap-3">
<h3 className="m-0 text-sm leading-5 font-medium text-foreground">{plan.name}</h3>
{plan.emphasised && (
<span className="inline-flex h-5 shrink-0 items-center rounded-sm bg-muted px-1.5 font-mono text-xs text-foreground">
Popular
</span>
)}
</div>
{plan.description && <p className="m-0 text-sm text-muted-foreground">{plan.description}</p>}
</div>
<p className="m-0 flex items-baseline gap-1">
<span className="text-3xl leading-none font-medium tracking-tight tabular-nums">{plan.price}</span>
{plan.period && <span className="font-mono text-xs text-muted-foreground">/{plan.period}</span>}
</p>
<ul className="m-0 flex list-none flex-col gap-2 p-0">
{plan.features.map((f, j) => (
<li key={j} className="flex items-start gap-2 text-sm text-foreground/85">
<CheckIcon className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
<span>{f}</span>
</li>
))}
</ul>
{plan.action && <div className="mt-auto flex flex-col *:w-full">{plan.action}</div>}
</li>
))}
</ul>
</section>
);
}
export type { Plan };
For AI agents
Hand your agent this page, the markdown at /components/pricing-table/prompt.md, or the Copy AI prompt button above.
When to use
- A pricing page or section with two to four tiers
- When one plan should read as the default choice
- Above a pricing-compare table for the detail
When not to use
- For a single plan or a donation, use a pricing card
- For forty line feature matrices, use pricing-compare
- As a settings screen for switching plans
Agent notes
- Server component; pass Buttons into each plan action
- Emphasise one plan only: it gets a foreground ring and a Popular mark, never a colour
- Columns follow the plan count: 2, 3 from md, 4 from lg; one column on phones
- Price is a formatted string; period is optional and draws as /period, leave it out for no suffix