Skip to content
ilinxa/pro-ui

Pricing Table

alphav0.2.0

Pricing tiers side by side — monthly and annual toggle, highlighted tier, per-feature tooltips, and a comparison layout. RTL-safe, full i18n.

Category: MarketingUpdated: 2026-08-11Created: 2026-05-22Author: ilinxa

Context

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.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/pricing-table

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/pricing-table-fixtures

Preview

Plans for every team

Start free, scale when you're ready.

Starter

For individuals trying things out.

$0
per month
Start free
  • Included:1 workspace
  • Included:Community support
  • Not included:
  • Not included:Custom domains
  • Not included:SLA
Most popular

Pro

For growing teams.

$19
per month
Start Pro trial
  • Included:Unlimited workspaces
  • Included:Priority email support
  • Included:Advanced analytics
  • Not included:Custom domains
  • Not included:SLA

Enterprise

For larger organizations.

$49
per month
Contact sales
  • Included:Unlimited workspaces
  • Included:Dedicated support
  • Included:Advanced analytics
  • Included:Custom domains
  • Included:

Demo source

demo.tsxtsx

Usage

When to use

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.

Minimal example

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 })
  }
/>;

CTA shape

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.

Billing toggle (controlled or uncontrolled)

// 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.

Comparison table

<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.

Free-tier label

<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.

Localization

<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}
/>;

Three tones

  • 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).

Accessibility

  • Root wraps content in a single <section> with aria-labelledby; works for both layouts.
  • Billing toggle is a WAI-ARIA role="radiogroup"; segments are role="radio"; arrow keys, Home, and End move focus + selection.
  • Tier cards are <article role="region"> with aria-labelledbypointing at the tier name. The "Most popular" badge announces via aria-label.
  • Feature rows are an <ul role="list">; check/x icons are aria-hidden; state is announced via sr-only labels.featureIncluded / featureExcluded.
  • Tooltips use shadcn Tooltip (Radix-backed) — open on focus + hover, dismiss on Escape and blur.

Development warnings

  • tiers.length < 2 or > 4 logs a dev-only warning (layout is designed for 2–4 tiers).
  • More than one tier marked highlighted logs a dev-only warning.
  • A CtaSpec with neither href nor onClick renders a disabled button + dev warn.

Features

  • 2 layout variants — cards (default, 2–4 tier grid) and table (feature-comparison grid, one column per tier)
  • Optional monthly/annual billing toggle (radiogroup, arrow-key + Home/End navigable)
  • 2–4 tiers via ReadonlyArray<PricingTier> with dev-mode length warning
  • Highlighted tier — signal-lime (`--primary`) accent border + ring + 'Most popular' badge (label overridable per-tier)
  • Per-feature included/excluded rows with optional shadcn Tooltip hint
  • Intl.NumberFormat price formatting pinned to en-US, ISO 4217 currencyCode, symbol-or-code display
  • Annual mode renders the per-month equivalent with strikethrough monthly when priceAnnual < priceMonthly + optional yearly-lump label via labels.yearlyHint
  • ReactNode CTAs (load-bearing) OR CtaSpec convenience overload ({ label, href?, onClick?, variant?, ariaLabel? })
  • Controlled-or-uncontrolled billing-period state (mirrors newsletter-signup's input convention)
  • Localizable labels bag with English defaults — toggle labels, sr-only a11y strings, period strings, optional freeLabel + yearlyHint
  • 3 tones — primary / accent / muted, palette aligned with newsletter-signup
  • Analytics hook (onTierCtaClick) — auto-fires for CtaSpec; consumer wires their own for ReactNode CTAs
  • Real <table> with scope=col/row semantics for the comparison layout; sticky-start first column on overflow scroll
  • React.memo wrapped

Tags

pricing-tablemarketingpricingconversioncms-blockform

Dependencies

shadcn primitives: button, tooltip
npm peer deps: lucide-react@^1.11.0