Pricing Tiers Toggle
React pricing section with three plans, a monthly and yearly toggle, odometer prices and a set-apart recommended plan.
- Pricing
- SaaS
- React only · 0 deps
- 1 file
Browser support · All evergreen browsers. color-mix() for focus rings (Chrome 111+, Safari 16.2+, Firefox 113+).
Install. One command, or copy the file.
$
npx shadcn@latest add https://freedesignmd.com/r/pricing-tiers-toggle.json- Byte-identical to the preview
- No Tailwind or config needed
- SSR-safe · keyboard accessible
- Respects reduced motion & transparency
components/pricing/pricing-tiers-toggle.tsx
1/**2 * PricingTiersToggle, three-plan B2B pricing section with a monthly / yearly3 * segmented switch. Prices roll like an odometer when the billing period4 * changes, a hand-drawn proof mark annotates the yearly saving, and the5 * recommended plan is set apart by structure (an inverted column that breaks6 * the top rule and carries its own tab), not by a glow.7 *8 * Props: plans (name, audience, monthly price, yearly price per month, limits,9 * features, cta, recommended?, note?), eyebrow, title, lede, currency,10 * unit (e.g. "editor / mo"), saveNote, footer (string[]), defaultPeriod.11 * CSS vars: --fd-ptt-paper, --fd-ptt-ink, --fd-ptt-signal.12 *13 * Browser support: all evergreen browsers.14 * Accessibility: the billing switch is a radiogroup (arrow keys move between15 * options); each price has a visually hidden full sentence and a polite live16 * region announces the period change. Odometer digits are aria-hidden.17 * Reduced motion: digits swap instantly, the switch thumb jumps.18 */19import { useId, useRef, useState } from "react";20import type { KeyboardEvent } from "react";21 22export type Period = "monthly" | "yearly";23 24export interface TierPlan {25 id: string;26 name: string;27 audience: string;28 /** Price per unit when billed monthly. */29 monthly: number;30 /** Price per unit per month when billed yearly. */31 yearly: number;32 limits: [string, string][];33 featuresTitle?: string;34 features: string[];35 cta: { label: string; href: string };36 recommended?: boolean;37 /** Short tab text shown on the recommended plan. */38 note?: string;39}40 41export interface PricingTiersToggleProps {42 plans?: TierPlan[];43 eyebrow?: string;44 title?: string;45 lede?: string;46 currency?: string;47 unit?: string;48 saveNote?: string;49 footer?: string[];50 defaultPeriod?: Period;51 className?: string;52}53 54const PLANS: TierPlan[] = [55 {56 id: "reviewer",57 name: "Reviewer",58 audience: "Freelancers and two-person studios shipping to a handful of clients.",59 monthly: 12,60 yearly: 10,61 limits: [62 ["Active projects", "3"],63 ["Version history", "30 days"],64 ["Guest reviewers", "5 per project"],65 ["Asset storage", "20 GB"],66 ],67 features: ["Review Figma frames, PDFs and live URLs", "Pin comments to pixels, not pages", "Export comments as CSV or PDF"],68 cta: { label: "Start 14-day trial", href: "#trial" },69 },70 {71 id: "studio",72 name: "Studio",73 audience: "In-house teams of 5 to 40 editors with a real approval process.",74 monthly: 24,75 yearly: 20,76 recommended: true,77 note: "Picked by 61% of teams with 10+ editors",78 limits: [79 ["Active projects", "Unlimited"],80 ["Version history", "1 year"],81 ["Guest reviewers", "Unlimited"],82 ["Asset storage", "200 GB"],83 ],84 featuresTitle: "Everything in Reviewer, plus",85 features: ["Approval stages with named sign-off (up to 3)", "Two-way sync with Slack, Linear and Jira", "Side-by-side version diff with overlay slider", "Due dates and reminders for late reviewers"],86 cta: { label: "Start 14-day trial", href: "#trial" },87 },88 {89 id: "agency",90 name: "Agency",91 audience: "Agencies running review rooms for many clients at once.",92 monthly: 48,93 yearly: 40,94 limits: [95 ["Client workspaces", "Unlimited, own branding"],96 ["Version history", "Forever"],97 ["Single sign-on", "SAML + SCIM"],98 ["Asset storage", "1 TB"],99 ],100 featuresTitle: "Everything in Studio, plus",101 features: ["Custom review domain (review.yourstudio.com)", "Audit log with 7-year retention", "99.9% uptime SLA, credits if we miss"],102 cta: { label: "Book a 20-minute demo", href: "#demo" },103 },104];105 106const FOOTER = ["Clients and stakeholders comment free, always", "14-day trial, no card", "Hosted in Frankfurt, EU data residency"];107 108function Odometer({ value }: { value: number }) {109 const s = String(value);110 return (111 <span className="fd-ptt-odo" aria-hidden="true">112 {s.split("").map((d, i) => (113 <span className="fd-ptt-dig" key={s.length - i}>114 <span className="fd-ptt-strip" style={{ transform: `translateY(${-Number(d) * 10}%)`, transitionDelay: `${i * 70}ms` }}>115 {"0123456789".split("").map((n) => (116 <span key={n}>{n}</span>117 ))}118 </span>119 </span>120 ))}121 </span>122 );123}124 125export function PricingTiersToggle({126 plans = PLANS,127 eyebrow = "Pricing, per editor seat",128 title = "Reviewers are free. You pay for the people who ship.",129 lede = "Every plan includes unlimited commenters. Count only the designers, writers and PMs who upload work.",130 currency = "$",131 unit = "editor / mo",132 saveNote = "2 months free",133 footer = FOOTER,134 defaultPeriod = "yearly",135 className,136}: PricingTiersToggleProps) {137 const [period, setPeriod] = useState<Period>(defaultPeriod);138 const [live, setLive] = useState("");139 const btns = useRef<(HTMLButtonElement | null)[]>([]);140 const uid = useId().replace(/[^a-zA-Z0-9]/g, "");141 const order: Period[] = ["monthly", "yearly"];142 143 const choose = (p: Period) => {144 setPeriod(p);145 setLive(p === "yearly" ? "Showing yearly prices, two months free." : "Showing monthly prices.");146 };147 const onKey = (e: KeyboardEvent) => {148 if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) return;149 e.preventDefault();150 const next = period === "monthly" ? "yearly" : "monthly";151 choose(next);152 btns.current[order.indexOf(next)]?.focus();153 };154 155 return (156 <section className={`fd-ptt${className ? " " + className : ""}`} aria-labelledby={`${uid}-t`}>157 <style>{css}</style>158 <div className="fd-ptt-head">159 <div className="fd-ptt-intro">160 <p className="fd-ptt-eyebrow">{eyebrow}</p>161 <h2 className="fd-ptt-title" id={`${uid}-t`}>{title}</h2>162 <p className="fd-ptt-lede">{lede}</p>163 </div>164 <div className="fd-ptt-billing">165 <div className="fd-ptt-seg" role="radiogroup" aria-label="Billing period" data-p={period} onKeyDown={onKey}>166 <span className="fd-ptt-thumb" aria-hidden="true" />167 {order.map((p, i) => (168 <button169 key={p}170 ref={(el) => {171 btns.current[i] = el;172 }}173 type="button"174 role="radio"175 aria-checked={period === p}176 tabIndex={period === p ? 0 : -1}177 className="fd-ptt-opt"178 onClick={() => choose(p)}179 >180 {p === "monthly" ? "Monthly" : "Yearly"}181 </button>182 ))}183 </div>184 <span className="fd-ptt-save" data-on={period === "yearly" ? "" : undefined}>185 <svg width="46" height="30" viewBox="0 0 46 30" fill="none" aria-hidden="true">186 <path className="fd-ptt-arrow" d="M44 26C30 27 14 22 7 6" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />187 <path className="fd-ptt-arrow" d="M3 11.5L6.6 5 12.5 9" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />188 </svg>189 <span>{saveNote}</span>190 </span>191 </div>192 </div>193 194 <ol className="fd-ptt-grid">195 {plans.map((p, idx) => {196 const price = period === "yearly" ? p.yearly : p.monthly;197 const billed = period === "yearly" ? `Billed ${currency}${p.yearly * 12} per editor yearly` : `Billed monthly, cancel any time`;198 return (199 <li key={p.id} className="fd-ptt-plan" data-rec={p.recommended ? "" : undefined}>200 {p.recommended && <p className="fd-ptt-tab">{p.note ?? "Recommended"}</p>}201 <div className="fd-ptt-top">202 <span className="fd-ptt-num">{String(idx + 1).padStart(2, "0")}</span>203 <h3 className="fd-ptt-name">{p.name}</h3>204 </div>205 <p className="fd-ptt-aud">{p.audience}</p>206 <div className="fd-ptt-price">207 <span className="fd-ptt-cur" aria-hidden="true">{currency}</span>208 <Odometer value={price} />209 <span className="fd-ptt-unit" aria-hidden="true">{unit}</span>210 <span className="fd-ptt-sr">211 {currency}212 {price} per {unit.replace("/", "per")}, {billed}213 </span>214 </div>215 <p className="fd-ptt-billed" aria-hidden="true">216 {period === "yearly" && <s>{currency}{p.monthly}</s>} {billed}217 </p>218 <a className="fd-ptt-cta" href={p.cta.href}>219 {p.cta.label}220 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">221 <path d="M3 7h8M7.5 3.5L11 7l-3.5 3.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />222 </svg>223 </a>224 <dl className="fd-ptt-limits">225 {p.limits.map(([k, v]) => (226 <div key={k}>227 <dt>{k}</dt>228 <dd>{v}</dd>229 </div>230 ))}231 </dl>232 {p.featuresTitle && <p className="fd-ptt-ftitle">{p.featuresTitle}</p>}233 <ul className="fd-ptt-feats">234 {p.features.map((f) => (235 <li key={f}>236 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">237 <path d="M2 7.6l3.1 3L12 3.2" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" />238 </svg>239 {f}240 </li>241 ))}242 </ul>243 </li>244 );245 })}246 </ol>247 248 <ul className="fd-ptt-foot">249 {footer.map((f) => (250 <li key={f}>{f}</li>251 ))}252 </ul>253 <div className="fd-ptt-sr" role="status" aria-live="polite">{live}</div>254 </section>255 );256}257 258const css = `259:where(.fd-ptt,.fd-ptt *){box-sizing:border-box;margin:0;padding:0;border:0;background:none;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}260:where(.fd-ptt,.fd-ptt *)::before,:where(.fd-ptt,.fd-ptt *)::after{box-sizing:border-box}261:where(.fd-ptt svg){display:block;flex-shrink:0}262:where(.fd-ptt button){appearance:none;-webkit-appearance:none;cursor:pointer;border-radius:0}263.fd-ptt{264 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-ease-in-out:cubic-bezier(.65,0,.35,1);--fd-spring:cubic-bezier(.34,1.56,.64,1);265 --fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;--fd-dur-4:700ms;266 --fd-ptt-paper:#F3F0E8;--fd-ptt-ink:#17160F;--fd-ptt-signal:#D9381E;267 --mut:#6B675C;--line:rgba(23,22,15,.14);--line2:rgba(23,22,15,.85);268 --sans:ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;269 --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;270 display:block;width:100%;background:var(--fd-ptt-paper);color:var(--fd-ptt-ink);271 font:400 15px/22px var(--sans);letter-spacing:-.003em;-webkit-font-smoothing:antialiased;272 padding:clamp(48px,7vw,96px) clamp(16px,4vw,56px) clamp(48px,6vw,80px)}273.fd-ptt-head{max-width:1200px;margin:0 auto;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:32px 48px;padding-bottom:clamp(48px,6vw,72px)}274.fd-ptt-eyebrow{font:500 12px/16px var(--mono);letter-spacing:.04em;text-transform:uppercase;color:var(--mut)}275.fd-ptt-title{margin-top:16px;max-width:15ch;font:700 clamp(2.25rem,5vw,4rem)/1 var(--sans);letter-spacing:-.045em}276.fd-ptt-lede{margin-top:20px;max-width:46ch;font-size:1.0625rem;line-height:26px;color:var(--mut)}277.fd-ptt-billing{position:relative;padding-bottom:6px}278.fd-ptt-seg{position:relative;display:grid;grid-template-columns:1fr 1fr;width:240px;height:48px;padding:4px;border-radius:9999px;background:#E6E1D4;box-shadow:inset 0 0 0 1px var(--line)}279.fd-ptt-thumb{position:absolute;top:4px;left:4px;width:calc(50% - 4px);height:40px;border-radius:9999px;background:var(--fd-ptt-ink);transition:transform var(--fd-dur-3) var(--fd-spring)}280.fd-ptt-seg[data-p="yearly"] .fd-ptt-thumb{transform:translateX(100%)}281.fd-ptt-opt{position:relative;z-index:1;height:40px;border-radius:9999px;font:600 14px/40px var(--sans);letter-spacing:-.005em;color:var(--fd-ptt-ink);transition:color var(--fd-dur-2) var(--fd-ease-in-out)}282.fd-ptt-opt[aria-checked="true"]{color:var(--fd-ptt-paper)}283.fd-ptt-opt:focus-visible{outline:2px solid color-mix(in srgb,var(--fd-ptt-signal) 70%,transparent);outline-offset:2px}284.fd-ptt-save{position:absolute;right:-8px;top:calc(100% + 2px);display:flex;align-items:flex-start;gap:4px;color:var(--fd-ptt-signal);font:italic 500 15px/18px "Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;white-space:nowrap;opacity:.35;transition:opacity var(--fd-dur-2)}285.fd-ptt-save span{padding-top:16px}286.fd-ptt-save[data-on]{opacity:1}287.fd-ptt-save svg{transform:translateX(-4px)}288.fd-ptt-arrow{stroke-dasharray:60;stroke-dashoffset:60;transition:stroke-dashoffset var(--fd-dur-4) var(--fd-ease-out)}289.fd-ptt-save[data-on] .fd-ptt-arrow{stroke-dashoffset:0}290 291.fd-ptt-grid{max-width:1200px;margin:0 auto;display:grid;grid-template-columns:1fr 1.12fr 1fr;border-top:1.5px solid var(--line2)}292.fd-ptt-plan{position:relative;display:flex;flex-direction:column;padding:32px clamp(20px,2.4vw,36px) 40px;border-left:1px solid var(--line)}293.fd-ptt-plan:first-child{border-left:0;padding-left:0}294.fd-ptt-plan:last-child{padding-right:0}295.fd-ptt-plan[data-rec]{margin-top:-28px;padding-top:28px;padding-left:clamp(24px,2.8vw,40px);padding-right:clamp(24px,2.8vw,40px);background:var(--fd-ptt-ink);color:var(--fd-ptt-paper);border-left:0;--mut:#ADA898;--line:rgba(243,240,232,.16)}296.fd-ptt-plan[data-rec]+.fd-ptt-plan{border-left:0}297.fd-ptt-tab{position:absolute;left:0;bottom:100%;padding:7px 12px 6px clamp(24px,2.8vw,40px);width:100%;background:var(--fd-ptt-signal);color:#fff;font:500 11px/16px var(--mono);letter-spacing:.03em;text-transform:uppercase}298.fd-ptt-top{display:flex;align-items:baseline;gap:12px}299.fd-ptt-num{font:500 12px/16px var(--mono);color:var(--mut)}300.fd-ptt-name{font:700 1.5rem/32px var(--sans);letter-spacing:-.025em}301.fd-ptt-aud{margin-top:8px;min-height:44px;max-width:34ch;font-size:.875rem;line-height:22px;color:var(--mut)}302.fd-ptt-price{position:relative;display:flex;align-items:flex-start;margin-top:28px;height:72px}303.fd-ptt-cur{font:600 1.5rem/32px var(--sans);margin-top:6px;margin-right:2px}304.fd-ptt-odo{display:flex;font:700 4.5rem/72px var(--sans);letter-spacing:-.05em;font-variant-numeric:tabular-nums}305.fd-ptt-dig{display:block;height:72px;overflow:hidden}306.fd-ptt-strip{display:flex;flex-direction:column;transition:transform var(--fd-dur-4) var(--fd-ease-in-out)}307.fd-ptt-strip span{display:block;height:72px}308.fd-ptt-unit{align-self:flex-end;margin:0 0 10px 8px;font:500 12px/16px var(--mono);color:var(--mut)}309.fd-ptt-billed{margin-top:8px;font:400 12px/18px var(--mono);color:var(--mut)}310.fd-ptt-billed s{text-decoration:line-through;text-decoration-color:var(--fd-ptt-signal);text-decoration-thickness:1.5px;margin-right:4px}311.fd-ptt-cta{display:inline-flex;align-items:center;justify-content:space-between;gap:12px;margin-top:24px;height:48px;padding:0 18px 0 20px;border-radius:9999px;font:600 15px/48px var(--sans);letter-spacing:-.01em;color:var(--fd-ptt-ink);box-shadow:inset 0 0 0 1.5px var(--fd-ptt-ink);transition:background-color var(--fd-dur-1),color var(--fd-dur-1),transform var(--fd-dur-2) var(--fd-spring)}312.fd-ptt-cta svg{transition:transform var(--fd-dur-2) var(--fd-ease-out)}313.fd-ptt-cta:hover{background:var(--fd-ptt-ink);color:var(--fd-ptt-paper)}314.fd-ptt-cta:hover svg{transform:translateX(3px)}315.fd-ptt-cta:active{transform:scale(.96);transition-duration:var(--fd-dur-1)}316.fd-ptt-cta:focus-visible{outline:2px solid color-mix(in srgb,var(--fd-ptt-signal) 70%,transparent);outline-offset:2px}317.fd-ptt-plan[data-rec] .fd-ptt-cta{background:var(--fd-ptt-paper);color:var(--fd-ptt-ink);box-shadow:none}318.fd-ptt-plan[data-rec] .fd-ptt-cta:hover{background:var(--fd-ptt-signal);color:#fff}319.fd-ptt-limits{margin-top:32px;border-top:1px solid var(--line)}320.fd-ptt-limits div{display:flex;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px dashed var(--line);font:400 12px/18px var(--mono)}321.fd-ptt-limits dt{color:var(--mut)}322.fd-ptt-limits dd{text-align:right;font-weight:600}323.fd-ptt-ftitle{margin-top:24px;font:500 11px/16px var(--mono);letter-spacing:.04em;text-transform:uppercase;color:var(--mut)}324.fd-ptt-feats{display:grid;gap:10px;margin-top:14px}325.fd-ptt-plan:not([data-rec]) .fd-ptt-feats:first-of-type{margin-top:24px}326.fd-ptt-feats li{display:flex;gap:10px;font-size:.875rem;line-height:20px}327.fd-ptt-feats svg{margin-top:3px;color:var(--fd-ptt-signal)}328.fd-ptt-foot{max-width:1200px;margin:0 auto;display:flex;flex-wrap:wrap;gap:8px 28px;padding-top:24px;border-top:1px solid var(--line);font:400 12px/18px var(--mono);color:var(--mut)}329.fd-ptt-foot li{display:flex;align-items:center;gap:8px}330.fd-ptt-foot li::before{content:"";display:block;width:6px;height:6px;background:var(--fd-ptt-signal)}331.fd-ptt-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}332@media (max-width:900px){333 .fd-ptt-head{grid-template-columns:1fr;padding-bottom:72px}334 .fd-ptt-grid{grid-template-columns:1fr;border-top:0}335 .fd-ptt-plan,.fd-ptt-plan:first-child,.fd-ptt-plan:last-child{padding:28px 0 36px;border-left:0;border-top:1.5px solid var(--line2)}336 .fd-ptt-plan[data-rec]{margin:40px -16px 0;padding:28px 16px 36px;border-top:0}337 .fd-ptt-plan[data-rec]+.fd-ptt-plan{border-top:0}338 .fd-ptt-tab{padding-left:16px}339 .fd-ptt-aud{min-height:0}340 .fd-ptt-foot{margin-top:8px}341}342@media (prefers-reduced-motion:reduce){.fd-ptt *{transition-duration:1ms!important;transition-delay:0ms!important}}343@media (prefers-contrast:more){.fd-ptt{--line:currentColor}}344`;345 346export function Demo() {347 return (348 <div className="fd-pttd">349 <style>{demoCss}</style>350 <header className="fd-pttd-nav">351 <a className="fd-pttd-logo" href="#home" aria-label="Redline home">352 <svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true">353 <rect x="1" y="1" width="20" height="20" stroke="currentColor" strokeWidth="2" />354 <path d="M-1 15L23 7" stroke="#D9381E" strokeWidth="2.4" />355 </svg>356 redline357 </a>358 <nav className="fd-pttd-links" aria-label="Main">359 <a href="#product">Product</a>360 <a href="#customers">Customers</a>361 <a href="#pricing" aria-current="page">Pricing</a>362 <a href="#changelog">Changelog</a>363 </nav>364 <a className="fd-pttd-sign" href="#login">Sign in</a>365 </header>366 <PricingTiersToggle />367 </div>368 );369}370 371const demoCss = `372:where(.fd-pttd,.fd-pttd *){box-sizing:border-box;margin:0;padding:0;font:inherit;color:inherit;text-decoration:none}373.fd-pttd{min-height:100vh;background:#F3F0E8;color:#17160F;font:400 14px/20px ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif}374.fd-pttd-nav{max-width:1312px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:24px;height:64px;padding:0 clamp(16px,4vw,56px);border-bottom:1px solid rgba(23,22,15,.14)}375.fd-pttd-logo{display:flex;align-items:center;gap:10px;font:800 19px/24px ui-sans-serif,-apple-system,BlinkMacSystemFont,"Inter",sans-serif;letter-spacing:-.05em}376.fd-pttd-logo svg{display:block;overflow:visible}377.fd-pttd-links{display:flex;gap:28px;font-weight:500;color:#6B675C}378.fd-pttd-links a[aria-current]{color:#17160F}379.fd-pttd-links a:hover{color:#17160F}380.fd-pttd-sign{font-weight:600}381@media (max-width:700px){.fd-pttd-links{display:none}}382`;More Pricing. Same file contract.
Questions about Pricing Tiers Toggle
Run npx shadcn@latest add https://freedesignmd.com/r/pricing-tiers-toggle.json, or open the page and copy the file into your project. It has zero dependencies besides React. What you copy is the exact file that renders the live demo.
React pricing section with three plans, a monthly and yearly toggle, odometer prices and a set-apart recommended plan. It is a free, MIT-licensed React pricing component from the freedesignmd library.
Yes. It is free and MIT-licensed, use, modify and ship it in personal and commercial projects without attribution.
All evergreen browsers. color-mix() for focus rings (Chrome 111+, Safari 16.2+, Firefox 113+).
Yes. It is a plain React component with its own styles, so it runs in Next.js, Vite, Remix and Lovable projects, and it needs no Tailwind setup. It has zero dependencies besides React.


