Starter
For individuals trying things out.
- Included:1 workspace
- Included:Community support
- Not included:
- Not included:Custom domains
- Not included:SLA
Pricing tiers side by side — monthly and annual toggle, highlighted tier, per-feature tooltips, and a comparison layout. RTL-safe, full i18n.
Second component in the marketing category. Greenfield (no migration). Mirrors newsletter-signup's controlled-or-uncontrolled state pattern and share-bar's analytics callback shape. Part of the CMS conversion-block batch (sibling: signup-form). Tiers accept ReactNode CTAs so consumers wrap with their own router primitive (registry can't import next/*); a CtaSpec convenience overload renders a plain anchor/button for the common case.
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/pricing-tableAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/pricing-table-fixturesStart free, scale when you're ready.
For individuals trying things out.
For growing teams.
For larger organizations.
Reach for PricingTablefor any marketing-page pricing block: 2–4 tier cards with an optional monthly/annual billing toggle and a highlighted "Most popular" tier. Switch to layout="table" for a feature-comparison grid when buyers scroll deep before deciding.
import { PricingTable } from "@/components/pricing-table";
<PricingTable
heading="Plans"
billingToggle="monthly-annual"
tiers={[starter, pro, enterprise]}
onTierCtaClick={(name) =>
analytics.track("pricing_cta_click", { tier: name })
}
/>;Each tier's cta accepts either a ReactNode (load-bearing — pass your router primitive, e.g. Next <Link>) or a CtaSpecconvenience overload. Registry code can't import next/*, so for SPA navigation you style your own element with buttonVariants:
import Link from "next/link";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
// CtaSpec — renders a plain <a> styled with buttonVariants
cta: { label: "Start free", href: "/signup", variant: "outline" }
// ReactNode — load-bearing, consumer styles the router primitive directly
// (no <Button asChild> — asChild is Radix-only and breaks Base UI installs)
cta: (
<Link href="/signup?plan=pro" className={cn(buttonVariants(), "w-full")}>
Start Pro trial
</Link>
)onTierCtaClick(tierName) auto-fires for CtaSpec tiers. For ReactNode tiers, wire your own click handler — analytics on a custom router primitive is your call.
// Uncontrolled (default)
<PricingTable
billingToggle="monthly-annual"
defaultBilling="annual"
tiers={tiers}
/>;
// Controlled — useful when toggle state lives in a CMS editor preview
const [billing, setBilling] = useState<BillingPeriod>("monthly");
<PricingTable
billingToggle="monthly-annual"
billing={billing}
onBillingChange={setBilling}
tiers={tiers}
/>;priceAnnual is the per-month rate when billed annually (not a yearly lump sum). When the toggle is in annual mode and priceAnnual < priceMonthly, the original monthly price renders alongside with strikethrough. A small yearly-lump label (derived as priceAnnual * 12) renders below the period string via labels.yearlyHint.
<PricingTable
heading="Compare plans"
layout="table"
billingToggle="monthly-annual"
tiers={tiers}
/>;Renders a real semantic <table> with <th scope="col"> per tier and <th scope="row"> per feature label. The first column is sticky-start on horizontal overflow.
<PricingTable
labels={{ freeLabel: "Free" }}
tiers={[{ ...starter, priceMonthly: 0 }, pro]}
/>;Opt-in: when priceMonthly === 0 and labels.freeLabel is set, the free label renders instead of the currency-formatted 0.
<PricingTable
labels={{
monthlyLabel: "Aylık",
annualLabel: "Yıllık",
toggleGroupLabel: "Ödeme dönemi",
popularBadge: "En popüler",
periodMonthly: "/ ay",
periodAnnual: "/ ay (yıllık ödeme)",
freeLabel: "Ücretsiz",
yearlyHint: "{amount} / yıl",
featureIncluded: "Dahil",
featureExcluded: "Dahil değil",
}}
tiers={tiers}
/>;primary (default) — signal-lime accent on highlighted tier ring + badge. Matches newsletter-signup's primary tone.accent — accent-tone framing when primary is already in play elsewhere on the page.muted — neutral, low-noise placement (docs / API products).<section> with aria-labelledby; works for both layouts.role="radiogroup"; segments are role="radio"; arrow keys, Home, and End move focus + selection.<article role="region"> with aria-labelledbypointing at the tier name. The "Most popular" badge announces via aria-label.<ul role="list">; check/x icons are aria-hidden; state is announced via sr-only labels.featureIncluded / featureExcluded.tiers.length < 2 or > 4 logs a dev-only warning (layout is designed for 2–4 tiers).highlighted logs a dev-only warning.CtaSpec with neither href nor onClick renders a disabled button + dev warn.