Pricing card
A plan with its price, a feature list and one action, alone or in a grid of plans.
npx shadcn@latest add https://ui.goodtabs.co/r/pricing-card.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/pricing-card
Free, Pro and Team in a PricingGrid, the middle one highlighted.
Single plan
One card on its own with a primary action.
Yearly with a footnote
period="year" and one quiet line under the action.
Usage
import { PricingCard, PricingGrid } from "@/components/ui/pricing-card"
import { Button } from "@/components/ui/button"
<PricingGrid>
<PricingCard name="Pro" price="$12" features={["Synced collections"]} action={<Button>Start a trial</Button>} highlighted />
</PricingGrid>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | The plan name. | |
| description | string | One line on who the plan is for. | |
| price | string | The price, already formatted, for example "$12". | |
| period | string | "month" | Billing period after the price, drawn as "/month". Pass "" for none. |
| features | string[] | What the plan includes, one short line each. | |
| action | ReactNode | The action, usually a Button. Stretched to the full width. | |
| highlighted | boolean | false | Draws a foreground border and a small Popular badge. |
| footnote | string | One quiet line under the action. | |
| columns | 2 | 3 | 3 | On PricingGrid: plans side by side from md up. |
Source
pricing-card/pricing-card.tsx
import * as React from "react";
import { CheckIcon } from "lucide-react";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { cn } from "@/lib/utils";
type PricingCardProps = Omit<React.ComponentProps<typeof Card>, "children"> & {
/** The plan name, for example "Pro". */
name: string;
/** One line on who the plan is for. */
description?: string;
/** The price, already formatted, for example "$12" or "Free". */
price: string;
/** Billing period after the price, drawn as a mono "/month". Pass an empty string for none. */
period?: string;
/** What the plan includes, one short line each. */
features?: string[];
/** The action, usually a Button. It is stretched to the full width. */
action?: React.ReactNode;
/** Draws a foreground border and a small Popular badge. */
highlighted?: boolean;
/** One quiet line under the action, for example the billing terms. */
footnote?: string;
};
/**
* A plan: name, price, features and one action. Monochrome; the highlighted plan
* gets a stronger border and a badge, not a colour.
*/
export function PricingCard({
name,
description,
price,
period = "month",
features,
action,
highlighted = false,
footnote,
className,
...props
}: PricingCardProps) {
return (
<Card
data-slot="pricing-card"
data-highlighted={highlighted || undefined}
className={cn("surface-shine gap-5 rounded-lg", highlighted && "ring-foreground", className)}
{...props}
>
<CardHeader className="gap-1">
<div className="flex items-center justify-between gap-3">
<h3 className="m-0 text-sm leading-5 font-medium text-foreground">{name}</h3>
{highlighted && (
<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>
{description && <p className="m-0 text-sm text-muted-foreground">{description}</p>}
</CardHeader>
<CardContent className="flex flex-col gap-5">
<p className="m-0 flex items-baseline gap-1">
<span className="text-3xl leading-none font-medium tracking-tight tabular-nums">{price}</span>
{period && <span className="font-mono text-xs text-muted-foreground">/{period}</span>}
</p>
{features && features.length > 0 && (
<ul className="m-0 flex list-none flex-col gap-2 p-0">
{features.map((feature) => (
<li key={feature} 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>{feature}</span>
</li>
))}
</ul>
)}
</CardContent>
{(action || footnote) && (
<CardContent className="mt-auto flex flex-col gap-3">
{action && <div className="flex flex-col *:w-full">{action}</div>}
{footnote && <p className="m-0 text-center text-xs text-muted-foreground">{footnote}</p>}
</CardContent>
)}
</Card>
);
}
type PricingGridProps = React.ComponentProps<"div"> & {
/** Plans side by side from md up. One column below. */
columns?: 2 | 3;
};
const COLUMNS = { 2: "md:grid-cols-2", 3: "md:grid-cols-3" } as const;
/** Two or three plans side by side with equal heights. */
export function PricingGrid({ columns = 3, className, ...props }: PricingGridProps) {
return <div data-slot="pricing-grid" className={cn("grid w-full gap-4", COLUMNS[columns], className)} {...props} />;
}
For AI agents
Hand your agent this page, the markdown at /components/pricing-card/prompt.md, or the Copy AI prompt button above.
When to use
- A pricing page or section with two or three plans side by side
- One plan on its own, for example an upgrade prompt inside the app
- Yearly and monthly variants of the same plans, switched by a toggle above
When not to use
- More than four plans or many feature rows, which want a comparison table
- Usage-based pricing with sliders and calculators
- A plan with no action to take
Agent notes
- Format the price before passing it; the card adds only the mono period after it
- The action is any element and is stretched to full width; pass a shadcn Button and pick its variant
- highlighted is monochrome by design: a foreground ring and a muted Popular badge, no accent colour
- Server component with no state; put the monthly and yearly toggle outside and swap the props