Skip to content
goodtabs UI
BrowseBrowse components

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

    Popular

    For 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

    Popular

    Shared 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

PropTypeDefaultDescription
plansPlan[]name, price, features and optional description, period, action and emphasised.
titlestringSection heading.
descriptionstringOne 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