Seat Stepper Pricing
Per-seat pricing stepper with hold-to-accelerate, a volume-discount scale, next price break flag and a sales handoff past 250.
- Pricing
- SaaS
- React only · 0 deps
- 1 file
Browser support · All evergreen browsers. color-mix() for the hint tint (Chrome 111+, Safari 16.2+, Firefox 113+).
Install. One command, or copy the file.
$
npx shadcn@latest add https://freedesignmd.com/r/seat-stepper-pricing.json- Byte-identical to the preview
- No Tailwind or config needed
- SSR-safe · keyboard accessible
- Respects reduced motion & transparency
components/pricing/seat-stepper-pricing.tsx
1/**2 * SeatStepperPricing, per-seat volume pricing for a team tool. A seat3 * stepper (buttons with press-and-hold acceleration, or type a number)4 * moves a marker along a visible volume-discount scale; crossing a break5 * re-prices every seat and flashes the tier. Monthly / annual switch. The6 * summary points out the next break ("add 6 seats and pay $84 less") and,7 * past the sales threshold, turns into a talk-to-sales card with a named8 * person.9 *10 * Props: tiers ({ from, monthly, annual }[]), salesFrom, defaultSeats,11 * defaultAnnual, title, lede, contact ({ name, role, photo, reply }), currency.12 * CSS vars: --fd-ssp-bg, --fd-ssp-ink, --fd-ssp-signal.13 *14 * Browser support: all evergreen browsers.15 * Accessibility: the input is a labelled spinbutton (ArrowUp/Down, PageUp/16 * PageDown by 10, Home/End); +/- have aria-labels; billing is a role="switch".17 * The total and tier changes are announced in a polite live region. The scale18 * is described in a visually hidden list.19 * Reduced motion: no marker spring, totals jump.20 */21import { useEffect, useId, useRef, useState } from "react";22import type { KeyboardEvent } from "react";23 24export interface SeatTier {25 from: number;26 monthly: number;27 annual: number;28}29 30export interface SeatStepperPricingProps {31 tiers?: SeatTier[];32 salesFrom?: number;33 defaultSeats?: number;34 defaultAnnual?: boolean;35 title?: string;36 lede?: string;37 currency?: string;38 contact?: { name: string; role: string; photo: string; reply: string; href: string };39 className?: string;40}41 42const TIERS: SeatTier[] = [43 { from: 1, monthly: 14, annual: 12 },44 { from: 10, monthly: 12, annual: 10 },45 { from: 50, monthly: 10, annual: 8.5 },46 { from: 100, monthly: 8.5, annual: 7 },47];48 49const SCALE_MAX = 400;50const pos = (n: number) => Math.min(1, Math.log(Math.max(1, n)) / Math.log(SCALE_MAX));51const fmt = (n: number, cur: string) => cur + n.toLocaleString("en-US", { minimumFractionDigits: n % 1 ? 2 : 0, maximumFractionDigits: 2 });52 53function useTween(target: number) {54 const [v, setV] = useState(target);55 const cur = useRef(target);56 useEffect(() => {57 if (matchMedia("(prefers-reduced-motion: reduce)").matches) {58 cur.current = target;59 setV(target);60 return;61 }62 const a = cur.current;63 const t0 = performance.now();64 let raf = 0;65 const tick = (now: number) => {66 const t = Math.min(1, (now - t0) / 420);67 const e = 1 - Math.pow(1 - t, 3);68 cur.current = a + (target - a) * e;69 setV(cur.current);70 if (t < 1) raf = requestAnimationFrame(tick);71 };72 raf = requestAnimationFrame(tick);73 return () => cancelAnimationFrame(raf);74 }, [target]);75 return v;76}77 78export function SeatStepperPricing({79 tiers = TIERS,80 salesFrom = 251,81 defaultSeats = 42,82 defaultAnnual = true,83 title = "Every seat gets cheaper as your team grows.",84 lede = "One plan, priced per person. Hit a break and the lower price applies to every seat, not only the new ones.",85 currency = "$",86 contact = { name: "Ines Moreau", role: "Head of Sales, EMEA", photo: "/img/people/p3.webp", reply: "Replies within 4 working hours", href: "#sales" },87 className,88}: SeatStepperPricingProps) {89 const uid = useId().replace(/[^a-zA-Z0-9]/g, "");90 const [seats, setSeats] = useState(defaultSeats);91 const [draft, setDraft] = useState(String(defaultSeats));92 const [annual, setAnnual] = useState(defaultAnnual);93 const [flash, setFlash] = useState(0);94 const hold = useRef<{ t?: ReturnType<typeof setTimeout>; n: number }>({ n: 0 });95 96 const tierIdx = tiers.reduce((acc, t, i) => (seats >= t.from ? i : acc), 0);97 const tier = tiers[tierIdx];98 const sales = seats >= salesFrom;99 const per = annual ? tier.annual : tier.monthly;100 const monthTotal = per * seats;101 const total = annual ? monthTotal * 12 : monthTotal;102 const shown = useTween(sales ? 0 : total);103 const prevTier = useRef(tierIdx);104 useEffect(() => {105 if (prevTier.current !== tierIdx) setFlash((f) => f + 1);106 prevTier.current = tierIdx;107 }, [tierIdx]);108 109 const next = tiers[tierIdx + 1];110 let hint = "";111 if (next && !sales) {112 const add = next.from - seats;113 const nextPer = annual ? next.annual : next.monthly;114 const nextMonth = nextPer * next.from;115 const diff = monthTotal - nextMonth;116 hint =117 diff > 0118 ? `Add ${add} seat${add > 1 ? "s" : ""} and you'd pay ${fmt(diff * (annual ? 12 : 1), currency)} less ${annual ? "a year" : "a month"}.`119 : `${add} more seat${add > 1 ? "s" : ""} and every seat drops to ${fmt(nextPer, currency)}.`;120 } else if (!sales) {121 hint = `Past ${salesFrom - 1} seats we'll quote you directly.`;122 }123 124 const set = (n: number) => {125 const v = Math.max(1, Math.min(1000, Math.round(n)));126 setSeats(v);127 setDraft(String(v));128 };129 const startHold = (d: number) => {130 set(seats + d);131 let s = seats + d;132 hold.current.n = 0;133 const loop = () => {134 hold.current.n++;135 const step = hold.current.n > 18 ? 10 : hold.current.n > 8 ? 5 : 1;136 s = Math.max(1, Math.min(1000, s + d * step));137 setSeats(s);138 setDraft(String(s));139 hold.current.t = setTimeout(loop, hold.current.n > 4 ? 70 : 140);140 };141 hold.current.t = setTimeout(loop, 380);142 };143 const stopHold = () => {144 if (hold.current.t) clearTimeout(hold.current.t);145 hold.current.t = undefined;146 };147 useEffect(() => stopHold, []);148 149 const onKey = (e: KeyboardEvent<HTMLInputElement>) => {150 const map: Record<string, number> = { ArrowUp: 1, ArrowDown: -1, PageUp: 10, PageDown: -10 };151 if (e.key in map) {152 e.preventDefault();153 set(seats + map[e.key]);154 } else if (e.key === "Home") {155 e.preventDefault();156 set(1);157 } else if (e.key === "End") {158 e.preventDefault();159 set(salesFrom);160 } else if (e.key === "Enter") {161 set(Number(draft) || 1);162 }163 };164 165 const bounds = [...tiers.map((t) => t.from), salesFrom, SCALE_MAX];166 167 return (168 <section className={`fd-ssp${className ? " " + className : ""}`} aria-labelledby={`${uid}-t`}>169 <style>{css}</style>170 <div className="fd-ssp-wrap">171 <div className="fd-ssp-main">172 <p className="fd-ssp-eyebrow">173 <span className="fd-ssp-sq" aria-hidden="true" /> Plinth Team plan174 </p>175 <h2 className="fd-ssp-title" id={`${uid}-t`}>{title}</h2>176 <p className="fd-ssp-lede">{lede}</p>177 178 <div className="fd-ssp-controls">179 <div className="fd-ssp-stepper">180 <label className="fd-ssp-lbl" htmlFor={`${uid}-n`}>Seats</label>181 <div className="fd-ssp-step">182 <button type="button" className="fd-ssp-btn" aria-label="Remove a seat" disabled={seats <= 1} onPointerDown={(e) => { e.preventDefault(); startHold(-1); }} onPointerUp={stopHold} onPointerLeave={stopHold} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); set(seats - 1); } }}>183 <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg>184 </button>185 <input186 id={`${uid}-n`}187 className="fd-ssp-input"188 type="text"189 inputMode="numeric"190 role="spinbutton"191 aria-valuenow={seats}192 aria-valuemin={1}193 aria-valuemax={1000}194 value={draft}195 onChange={(e) => {196 const v = e.target.value.replace(/[^0-9]/g, "").slice(0, 4);197 setDraft(v);198 if (v) setSeats(Math.max(1, Math.min(1000, Number(v))));199 }}200 onBlur={() => set(Number(draft) || 1)}201 onKeyDown={onKey}202 />203 <button type="button" className="fd-ssp-btn" aria-label="Add a seat" onPointerDown={(e) => { e.preventDefault(); startHold(1); }} onPointerUp={stopHold} onPointerLeave={stopHold} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); set(seats + 1); } }}>204 <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h10M8 3v10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg>205 </button>206 </div>207 <p className="fd-ssp-holdhint">Hold to count faster</p>208 </div>209 210 <button type="button" role="switch" aria-checked={annual} className="fd-ssp-switch" onClick={() => setAnnual((a) => !a)}>211 <span className="fd-ssp-track" aria-hidden="true"><span /></span>212 <span className="fd-ssp-sw-t">213 Billed annually214 <small>{annual ? "2 months free" : "Switch to save ~17%"}</small>215 </span>216 </button>217 </div>218 219 <div className="fd-ssp-scale" style={{ ["--x" as string]: pos(Math.min(seats, SCALE_MAX)) }}>220 <div className="fd-ssp-bar" aria-hidden="true">221 {bounds.slice(0, -1).map((b, i) => {222 const l = pos(b);223 const r = pos(bounds[i + 1]);224 const isSales = i === tiers.length;225 const active = sales ? isSales : i === tierIdx;226 const passed = !isSales && i < tierIdx;227 return (228 <span key={b} className="fd-ssp-seg" data-on={active ? "" : undefined} data-past={passed || (sales && !isSales) ? "" : undefined} data-sales={isSales ? "" : undefined} style={{ left: `${l * 100}%`, width: `${(r - l) * 100}%` }}>229 {active && <i key={flash} className="fd-ssp-sweep" />}230 <b>{isSales ? "Sales" : fmt(annual ? tiers[i].annual : tiers[i].monthly, currency)}</b>231 <em>{isSales ? `${salesFrom}+` : `${b}${tiers[i + 1] ? `–${tiers[i + 1].from - 1}` : `–${salesFrom - 1}`}`}</em>232 </span>233 );234 })}235 <span className="fd-ssp-marker" data-over={seats > SCALE_MAX ? "" : undefined}>236 <span>{seats}</span>237 </span>238 </div>239 <ul className="fd-ssp-sr">240 {tiers.map((t, i) => (241 <li key={t.from}>242 {t.from} to {(tiers[i + 1]?.from ?? salesFrom) - 1} seats: {fmt(annual ? t.annual : t.monthly, currency)} per seat per month243 </li>244 ))}245 <li>{salesFrom} seats or more: custom pricing</li>246 </ul>247 </div>248 </div>249 250 <aside className="fd-ssp-sum" data-sales={sales ? "" : undefined} aria-label="Your price">251 {!sales ? (252 <>253 <p className="fd-ssp-sum-l">Per seat, per month</p>254 <p className="fd-ssp-per" key={`${per}`}>255 {fmt(per, currency)}256 </p>257 <dl className="fd-ssp-math">258 <div>259 <dt>Seats</dt>260 <dd>× {seats}</dd>261 </div>262 {annual && (263 <div>264 <dt>Months</dt>265 <dd>× 12</dd>266 </div>267 )}268 <div className="fd-ssp-tot">269 <dt>{annual ? "Per year" : "Per month"}</dt>270 <dd>{currency}{Math.round(shown).toLocaleString("en-US")}</dd>271 </div>272 </dl>273 {hint && <p className="fd-ssp-hint">{hint}</p>}274 <a className="fd-ssp-cta" href="#start">Start with {seats} seat{seats > 1 ? "s" : ""}</a>275 <p className="fd-ssp-fine">Add or remove people any time; we prorate to the day. Prices exclude VAT.</p>276 </>277 ) : (278 <div className="fd-ssp-sales">279 <p className="fd-ssp-sum-l">{seats} seats</p>280 <p className="fd-ssp-sales-t">At this size, let's talk.</p>281 <p className="fd-ssp-sales-d">Teams over {salesFrom - 1} get a custom rate, SSO and audit logs at no extra cost, and a migration plan for your old wiki.</p>282 <div className="fd-ssp-person">283 <img src={contact.photo} width={48} height={48} alt="" loading="lazy" />284 <span>285 <b>{contact.name}</b>286 {contact.role}287 <small>{contact.reply}</small>288 </span>289 </div>290 <a className="fd-ssp-cta" href={contact.href}>Talk to sales</a>291 </div>292 )}293 </aside>294 </div>295 <div className="fd-ssp-sr" role="status" aria-live="polite">296 {sales ? `${seats} seats: custom pricing, talk to sales.` : `${seats} seats at ${fmt(per, currency)} each, ${currency}${total.toLocaleString("en-US")} ${annual ? "per year" : "per month"}.`}297 </div>298 </section>299 );300}301 302const css = `303:where(.fd-ssp,.fd-ssp *){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}304:where(.fd-ssp,.fd-ssp *)::before,:where(.fd-ssp,.fd-ssp *)::after{box-sizing:border-box}305:where(.fd-ssp svg,.fd-ssp img){display:block;flex-shrink:0}306:where(.fd-ssp button){appearance:none;-webkit-appearance:none;cursor:pointer;text-align:left}307.fd-ssp{308 --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);309 --fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;--fd-dur-4:700ms;310 --fd-ssp-bg:#FAFAF8;--fd-ssp-ink:#111110;--fd-ssp-signal:#FF4D00;311 --mut:#6F6F6A;--line:#E3E3DE;--soft:#EFEFEB;312 --sans:ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;313 display:block;width:100%;background:var(--fd-ssp-bg);color:var(--fd-ssp-ink);font:400 15px/22px var(--sans);-webkit-font-smoothing:antialiased;314 padding:clamp(48px,7vw,96px) clamp(16px,4vw,56px)}315.fd-ssp-wrap{max-width:1200px;margin:0 auto;display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:clamp(32px,5vw,80px);align-items:start}316.fd-ssp-eyebrow{display:flex;align-items:center;gap:8px;font:600 13px/20px var(--sans)}317.fd-ssp-sq{display:block;width:10px;height:10px;background:var(--fd-ssp-signal)}318.fd-ssp-title{margin-top:16px;max-width:17ch;font:650 clamp(2.25rem,4.6vw,3.75rem)/1.02 var(--sans);font-weight:650;letter-spacing:-.045em}319.fd-ssp-lede{margin-top:18px;max-width:50ch;font-size:1.0625rem;line-height:26px;color:var(--mut)}320.fd-ssp-controls{display:flex;flex-wrap:wrap;align-items:flex-end;gap:24px 40px;margin-top:44px}321.fd-ssp-lbl{display:block;font:500 13px/18px var(--sans);color:var(--mut)}322.fd-ssp-step{display:flex;align-items:center;gap:4px;margin-top:8px;padding:4px;border-radius:16px;background:#fff;box-shadow:0 0 0 1px var(--line),0 1px 2px rgba(0,0,0,.04)}323.fd-ssp-btn{display:grid;place-items:center;width:52px;height:52px;border-radius:12px;color:var(--fd-ssp-ink);background:var(--soft);transition:background-color var(--fd-dur-1),transform var(--fd-dur-2) var(--fd-spring);touch-action:manipulation;user-select:none;-webkit-user-select:none}324.fd-ssp-btn:hover{background:#E4E4DF}325.fd-ssp-btn:active{transform:scale(.96);transition-duration:var(--fd-dur-1)}326.fd-ssp-btn:disabled{opacity:.35;cursor:not-allowed}327.fd-ssp-btn:focus-visible,.fd-ssp-input:focus-visible,.fd-ssp-switch:focus-visible,.fd-ssp-cta:focus-visible{outline:2px solid color-mix(in srgb,var(--fd-ssp-signal) 70%,transparent);outline-offset:2px}328.fd-ssp-input{width:112px;height:52px;border-radius:10px;text-align:center;font:700 2rem/52px var(--sans);letter-spacing:-.04em;font-variant-numeric:tabular-nums;color:var(--fd-ssp-ink);background:transparent;outline:none}329.fd-ssp-holdhint{margin-top:8px;font:400 12px/16px var(--sans);color:var(--mut)}330.fd-ssp-switch{display:flex;align-items:center;gap:12px;padding-bottom:30px}331.fd-ssp-track{position:relative;display:block;width:44px;height:26px;border-radius:9999px;background:#D6D6D0;transition:background-color var(--fd-dur-2)}332.fd-ssp-track span{position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:transform var(--fd-dur-2) var(--fd-spring)}333.fd-ssp-switch[aria-checked="true"] .fd-ssp-track{background:var(--fd-ssp-ink)}334.fd-ssp-switch[aria-checked="true"] .fd-ssp-track span{transform:translateX(18px)}335.fd-ssp-sw-t{display:flex;flex-direction:column;font:600 14px/20px var(--sans)}336.fd-ssp-sw-t small{font-weight:400;font-size:12px;line-height:16px;color:var(--mut)}337.fd-ssp-switch[aria-checked="true"] small{color:var(--fd-ssp-signal);font-weight:600}338 339.fd-ssp-scale{margin-top:36px;padding-top:44px}340.fd-ssp-bar{position:relative;height:96px}341.fd-ssp-seg{position:absolute;top:0;height:12px;background:var(--soft);box-shadow:inset 1px 0 0 var(--fd-ssp-bg);overflow:visible;transition:background-color var(--fd-dur-2)}342.fd-ssp-seg:first-of-type{border-radius:6px 0 0 6px}343.fd-ssp-seg[data-sales]{border-radius:0 6px 6px 0;background:repeating-linear-gradient(-45deg,var(--soft) 0 4px,var(--fd-ssp-bg) 4px 8px)}344.fd-ssp-seg[data-past]{background:#CFCFC9}345.fd-ssp-seg[data-on]{background:var(--fd-ssp-ink)}346.fd-ssp-seg[data-on][data-sales]{background:var(--fd-ssp-signal)}347.fd-ssp-sweep{position:absolute;inset:0;background:var(--fd-ssp-signal);transform-origin:left;animation:fd-ssp-sweep var(--fd-dur-4) var(--fd-ease-out) forwards}348@keyframes fd-ssp-sweep{0%{transform:scaleX(0);opacity:1}60%{transform:scaleX(1);opacity:1}100%{transform:scaleX(1);opacity:0}}349.fd-ssp-seg b,.fd-ssp-seg em{position:absolute;left:8px;white-space:nowrap;font-style:normal}350.fd-ssp-seg b{top:24px;font:700 17px/24px var(--sans);letter-spacing:-.02em;color:var(--mut);font-variant-numeric:tabular-nums;transition:color var(--fd-dur-2)}351.fd-ssp-seg em{top:48px;font:400 12px/16px var(--sans);color:var(--mut)}352.fd-ssp-seg[data-on] b{color:var(--fd-ssp-ink)}353.fd-ssp-seg[data-on][data-sales] b{color:var(--fd-ssp-signal)}354.fd-ssp-seg::before{content:"";position:absolute;left:0;top:12px;height:56px;width:1px;background:var(--line)}355.fd-ssp-marker{position:absolute;top:-38px;left:calc(var(--x) * 100%);transform:translateX(-50%);transition:left var(--fd-dur-3) var(--fd-spring);z-index:2}356.fd-ssp-marker span{display:block;padding:3px 8px;border-radius:6px;background:var(--fd-ssp-signal);color:#fff;font:700 12px/16px var(--sans);font-variant-numeric:tabular-nums;white-space:nowrap}357.fd-ssp-marker::after{content:"";position:absolute;left:50%;top:100%;width:2px;height:24px;margin-left:-1px;background:var(--fd-ssp-signal)}358.fd-ssp-marker[data-over]{transform:translateX(-100%)}359 360.fd-ssp-sum{position:relative;padding:28px;border-radius:24px;background:#fff;box-shadow:0 0 0 1px var(--line),0 20px 40px -28px rgba(0,0,0,.25)}361.fd-ssp-sum-l{font:500 13px/18px var(--sans);color:var(--mut)}362.fd-ssp-per{margin-top:6px;font:700 clamp(3.5rem,5vw,4.5rem)/1 var(--sans);letter-spacing:-.05em;font-variant-numeric:tabular-nums;animation:fd-ssp-pop var(--fd-dur-3) var(--fd-spring)}363@keyframes fd-ssp-pop{from{transform:translateY(8px);opacity:.2}to{transform:none;opacity:1}}364.fd-ssp-math{margin-top:24px;border-top:1px solid var(--line)}365.fd-ssp-math div{display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}366.fd-ssp-math dt{color:var(--mut)}367.fd-ssp-math dd{font-weight:600}368.fd-ssp-math .fd-ssp-tot{border-bottom:0;padding-top:14px}369.fd-ssp-tot dt{color:var(--fd-ssp-ink);font-weight:600}370.fd-ssp-tot dd{font:700 1.5rem/28px var(--sans);letter-spacing:-.03em}371.fd-ssp-hint{margin-top:8px;padding:10px 12px;border-radius:10px;background:color-mix(in srgb,var(--fd-ssp-signal) 9%,#fff);color:#A63200;font:500 13px/18px var(--sans)}372.fd-ssp-cta{display:flex;align-items:center;justify-content:center;margin-top:20px;height:52px;border-radius:14px;background:var(--fd-ssp-ink);color:#fff;font:600 15px/52px var(--sans);transition:background-color var(--fd-dur-1),transform var(--fd-dur-2) var(--fd-spring)}373.fd-ssp-cta:hover{background:#2A2A27}374.fd-ssp-cta:active{transform:scale(.96);transition-duration:var(--fd-dur-1)}375.fd-ssp-sum[data-sales] .fd-ssp-cta{background:var(--fd-ssp-signal)}376.fd-ssp-fine{margin-top:12px;font:400 12px/17px var(--sans);color:var(--mut)}377.fd-ssp-sales{animation:fd-ssp-pop var(--fd-dur-3) var(--fd-ease-out)}378.fd-ssp-sales-t{margin-top:6px;font:700 2rem/36px var(--sans);letter-spacing:-.035em}379.fd-ssp-sales-d{margin-top:10px;color:var(--mut)}380.fd-ssp-person{display:flex;align-items:center;gap:12px;margin-top:20px;padding:12px;border-radius:14px;background:var(--soft)}381.fd-ssp-person img{width:48px;height:48px;border-radius:50%;object-fit:cover}382.fd-ssp-person span{display:flex;flex-direction:column;font:400 13px/18px var(--sans);color:var(--mut)}383.fd-ssp-person b{font-weight:600;color:var(--fd-ssp-ink);font-size:14px}384.fd-ssp-person small{margin-top:2px;font-size:12px;color:#1F7A3A}385.fd-ssp-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}386@media (max-width:960px){.fd-ssp-wrap{grid-template-columns:1fr}}387@media (max-width:560px){388 .fd-ssp-seg b{font-size:13px;left:4px}389 .fd-ssp-seg em{font-size:10px;left:4px}390 .fd-ssp-seg[data-sales] b,.fd-ssp-seg[data-sales] em{left:auto;right:0}391 .fd-ssp-switch{padding-bottom:0}392 .fd-ssp-sum{padding:22px}393}394@media (prefers-reduced-motion:reduce){.fd-ssp *{transition-duration:1ms!important;animation:none!important}}395@media (prefers-contrast:more){.fd-ssp{--line:#111110}}396`;397 398export function Demo() {399 return (400 <div className="fd-sspd">401 <style>{demoCss}</style>402 <header className="fd-sspd-nav">403 <a href="#home" className="fd-sspd-logo">404 <svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true">405 <rect x="2" y="12" width="16" height="6" fill="#111110" />406 <rect x="5" y="6" width="10" height="6" fill="#111110" />407 <rect x="8" y="1" width="4" height="5" fill="#FF4D00" />408 </svg>409 Plinth410 </a>411 <nav aria-label="Main">412 <a href="#product">Product</a>413 <a href="#pricing" aria-current="page">Pricing</a>414 <a href="#security">Security</a>415 </nav>416 <a href="#login" className="fd-sspd-in">Log in</a>417 </header>418 <SeatStepperPricing />419 </div>420 );421}422 423const demoCss = `424:where(.fd-sspd,.fd-sspd *){box-sizing:border-box;margin:0;padding:0;font:inherit;color:inherit;text-decoration:none}425.fd-sspd{min-height:100vh;background:#FAFAF8;color:#111110;font:400 14px/20px ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif}426.fd-sspd-nav{display:flex;align-items:center;justify-content:space-between;gap:20px;height:60px;padding:0 clamp(16px,4vw,56px);border-bottom:1px solid #E3E3DE}427.fd-sspd-logo{display:flex;align-items:center;gap:8px;font-weight:700;font-size:17px;letter-spacing:-.03em}428.fd-sspd-logo svg{display:block}429.fd-sspd-nav nav{display:flex;gap:24px;color:#6F6F6A;font-weight:500}430.fd-sspd-nav nav a[aria-current]{color:#111110}431.fd-sspd-in{font-weight:600}432@media (max-width:640px){.fd-sspd-nav nav{display:none}}433`;More Pricing. Same file contract.
Questions about Seat Stepper Pricing
Run npx shadcn@latest add https://freedesignmd.com/r/seat-stepper-pricing.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.
Per-seat pricing stepper with hold-to-accelerate, a volume-discount scale, next price break flag and a sales handoff past 250. 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 the hint tint (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.


