Skip to content

Usage Calculator

Usage-based pricing calculator: snapping sliders price every plan at once, flag the cheapest and animate an itemised bill.

  • Pricing
  • Developer tools
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers. Styled native range inputs (WebKit and Gecko thumb selectors).

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/usage-calculator.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/pricing/usage-calculator.tsx
1/**
2 * UsageCalculator, usage-based API / infrastructure pricing. Three snapping
3 * sliders (requests, storage, seats) price the same usage under every plan,
4 * pick the cheapest one that fits ("You're on Scale"), and show an itemised
5 * bill whose total counts to its new value. A stacked bar breaks the bill
6 * down and a small comparison shows what the other plans would have cost.
7 *
8 * Props: plans (base, included quotas, overage rates, limits), defaults
9 * ({ requests, storage, seats } as stop indexes / seat count), requestStops,
10 * storageStops, maxSeats, title, lede.
11 * CSS vars: --fd-uc-bg, --fd-uc-fg, --fd-uc-signal.
12 *
13 * Browser support: all evergreen browsers (styled native range inputs).
14 * Accessibility: native <input type="range"> with aria-valuetext ("400
15 * million requests"); the total sits in a polite live region that updates
16 * after the animation settles. Bars are aria-hidden; the itemised list carries
17 * the same numbers as text.
18 * Reduced motion: the total jumps, bars resize without transition.
19 */
20import { useEffect, useId, useMemo, useRef, useState } from "react";
21
22export interface UsagePlan {
23 id: string;
24 name: string;
25 base: number;
26 incRequests: number; // millions
27 incStorage: number; // GB
28 incSeats: number;
29 perMillion: number | null; // null = hard limit
30 perGB: number | null;
31 perSeat: number | null;
32 maxRequests: number; // millions
33 maxSeats: number;
34}
35
36export interface UsageCalculatorProps {
37 plans?: UsagePlan[];
38 requestStops?: number[];
39 storageStops?: number[];
40 maxSeats?: number;
41 defaults?: { requests: number; storage: number; seats: number };
42 title?: string;
43 lede?: string;
44 className?: string;
45}
46
47const PLANS: UsagePlan[] = [
48 { id: "hobby", name: "Hobby", base: 0, incRequests: 1, incStorage: 5, incSeats: 1, perMillion: null, perGB: null, perSeat: null, maxRequests: 1, maxSeats: 1 },
49 { id: "pro", name: "Pro", base: 29, incRequests: 10, incStorage: 100, incSeats: 3, perMillion: 1.2, perGB: 0.2, perSeat: 12, maxRequests: 1000, maxSeats: 20 },
50 { id: "scale", name: "Scale", base: 499, incRequests: 200, incStorage: 1000, incSeats: 10, perMillion: 0.3, perGB: 0.06, perSeat: 8, maxRequests: 1000, maxSeats: 40 },
51];
52
53const REQ = [1, 5, 10, 25, 50, 100, 200, 400, 600, 800, 1000];
54const STO = [5, 25, 50, 100, 250, 500, 750, 1000, 2000, 3000];
55
56const fmtReq = (m: number) => (m >= 1000 ? `${m / 1000}B` : `${m}M`);
57const fmtSto = (g: number) => (g >= 1000 ? `${g / 1000} TB` : `${g} GB`);
58const money = (n: number, d = 2) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
59
60type Line = { key: "base" | "req" | "sto" | "seat"; label: string; detail: string; amount: number };
61
62function price(p: UsagePlan, req: number, sto: number, seats: number): { ok: boolean; lines: Line[]; total: number } {
63 const xr = Math.max(0, req - p.incRequests);
64 const xs = Math.max(0, sto - p.incStorage);
65 const xu = Math.max(0, seats - p.incSeats);
66 const ok =
67 req <= p.maxRequests &&
68 seats <= p.maxSeats &&
69 (p.perMillion !== null || xr === 0) &&
70 (p.perGB !== null || xs === 0) &&
71 (p.perSeat !== null || xu === 0);
72 const lines: Line[] = [
73 { key: "base", label: `${p.name} platform fee`, detail: `incl. ${fmtReq(p.incRequests)} req, ${fmtSto(p.incStorage)}, ${p.incSeats} seat${p.incSeats > 1 ? "s" : ""}`, amount: p.base },
74 { key: "req", label: "Requests", detail: xr ? `${fmtReq(xr)} over × $${(p.perMillion ?? 0).toFixed(2)}/M` : "within included", amount: xr * (p.perMillion ?? 0) },
75 { key: "sto", label: "Storage", detail: xs ? `${xs.toLocaleString("en-US")} GB over × $${(p.perGB ?? 0).toFixed(2)}` : "within included", amount: xs * (p.perGB ?? 0) },
76 { key: "seat", label: "Seats", detail: xu ? `${xu} extra × $${p.perSeat}` : "within included", amount: xu * (p.perSeat ?? 0) },
77 ];
78 return { ok, lines, total: lines.reduce((a, l) => a + l.amount, 0) };
79}
80
81function useTween(target: number) {
82 const [v, setV] = useState(target);
83 const from = useRef(target);
84 useEffect(() => {
85 const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
86 const start = from.current;
87 if (reduce || start === target) {
88 from.current = target;
89 setV(target);
90 return;
91 }
92 let raf = 0;
93 const t0 = performance.now();
94 const D = 420;
95 const tick = (now: number) => {
96 const t = Math.min(1, (now - t0) / D);
97 const e = 1 - Math.pow(1 - t, 4);
98 const cur = start + (target - start) * e;
99 from.current = cur;
100 setV(cur);
101 if (t < 1) raf = requestAnimationFrame(tick);
102 };
103 raf = requestAnimationFrame(tick);
104 return () => cancelAnimationFrame(raf);
105 }, [target]);
106 return v;
107}
108
109function Slider({ label, value, stops, onChange, fmt, valueText, unit, labels }: { label: string; value: number; stops: number[]; onChange: (i: number) => void; fmt: (n: number) => string; valueText: (n: number) => string; unit: string; labels: number[] }) {
110 const id = useId();
111 const pct = (value / (stops.length - 1)) * 100;
112 return (
113 <div className="fd-uc-sl">
114 <div className="fd-uc-sl-top">
115 <label htmlFor={id}>{label}</label>
116 <output htmlFor={id}>
117 {fmt(stops[value])}
118 <small>{unit}</small>
119 </output>
120 </div>
121 <div className="fd-uc-track" style={{ ["--p" as string]: `${pct}%` }}>
122 <input
123 id={id}
124 type="range"
125 min={0}
126 max={stops.length - 1}
127 step={1}
128 value={value}
129 aria-valuetext={valueText(stops[value])}
130 onChange={(e) => onChange(Number(e.target.value))}
131 />
132 <div className="fd-uc-ticks" aria-hidden="true">
133 {stops.map((s, i) => (
134 <span key={s} data-on={i <= value ? "" : undefined} style={{ left: `${(i / (stops.length - 1)) * 100}%` }}>
135 {labels.includes(i) ? <em>{fmt(s)}</em> : null}
136 </span>
137 ))}
138 </div>
139 </div>
140 </div>
141 );
142}
143
144const COLORS: Record<Line["key"], string> = { base: "#F2EFE6", req: "#FFB224", sto: "#7FA6FF", seat: "#5ED1A0" };
145
146export function UsageCalculator({
147 plans = PLANS,
148 requestStops = REQ,
149 storageStops = STO,
150 maxSeats = 40,
151 defaults = { requests: 7, storage: 6, seats: 12 },
152 title = "Pay for what your functions actually do.",
153 lede = "Move the sliders to your last month. We price it on every plan and put you on the cheapest one. No commitment, plans switch automatically at the start of each billing cycle.",
154 className,
155}: UsageCalculatorProps) {
156 const [ri, setRi] = useState(defaults.requests);
157 const [si, setSi] = useState(defaults.storage);
158 const [seats, setSeats] = useState(defaults.seats);
159 const req = requestStops[ri];
160 const sto = storageStops[si];
161
162 const priced = useMemo(() => plans.map((p) => ({ plan: p, ...price(p, req, sto, seats) })), [plans, req, sto, seats]);
163 const valid = priced.filter((p) => p.ok).sort((a, b) => a.total - b.total);
164 const best = valid[0];
165 const total = best ? best.total : 0;
166 const shown = useTween(total);
167 const [live, setLive] = useState("");
168 useEffect(() => {
169 const t = setTimeout(() => setLive(best ? `${best.plan.name} plan, ${money(best.total)} per month` : "Enterprise pricing, talk to us"), 500);
170 return () => clearTimeout(t);
171 }, [best?.plan.id, best?.total]); // eslint-disable-line react-hooks/exhaustive-deps
172
173 const runnerUp = best ? priced.filter((p) => p.plan.id !== best.plan.id && p.ok).sort((a, b) => a.total - b.total)[0] : undefined;
174 const [whole, cents] = money(shown).split(".");
175
176 return (
177 <section className={`fd-uc${className ? " " + className : ""}`} aria-label="Usage pricing calculator">
178 <style>{css}</style>
179 <div className="fd-uc-wrap">
180 <div className="fd-uc-left">
181 <p className="fd-uc-eyebrow">
182 <span>$ kestrel pricing --estimate</span>
183 </p>
184 <h2 className="fd-uc-title">{title}</h2>
185 <p className="fd-uc-lede">{lede}</p>
186 <div className="fd-uc-sliders">
187 <Slider label="Requests / month" unit="req" labels={[0, 2, 4, 6, 8, 10]} value={ri} stops={requestStops} onChange={setRi} fmt={fmtReq} valueText={(n) => (n >= 1000 ? `${n / 1000} billion requests` : `${n} million requests`)} />
188 <Slider label="Storage (KV + blobs)" unit="" labels={[0, 2, 4, 6, 9]} value={si} stops={storageStops} onChange={setSi} fmt={fmtSto} valueText={(n) => fmtSto(n)} />
189 <Slider
190 label="Team seats"
191 unit={seats === 1 ? "seat" : "seats"}
192 value={seats - 1}
193 labels={[0, 9, 19, 29, maxSeats - 1]}
194 stops={Array.from({ length: maxSeats }, (_, i) => i + 1)}
195 onChange={(i) => setSeats(i + 1)}
196 fmt={(n) => String(n)}
197 valueText={(n) => `${n} seats`}
198 />
199 </div>
200 </div>
201
202 <div className="fd-uc-bill" data-plan={best?.plan.id ?? "enterprise"}>
203 <div className="fd-uc-detect">
204 <span className="fd-uc-led" aria-hidden="true" />
205 <span className="fd-uc-detect-t">
206 You&apos;re on <b key={best?.plan.id ?? "ent"}>{best ? best.plan.name : "Enterprise"}</b>
207 </span>
208 {best && runnerUp && <span className="fd-uc-saves">saves {money(runnerUp.total - best.total, 0)} vs {runnerUp.plan.name}</span>}
209 </div>
210
211 {best ? (
212 <>
213 <ul className="fd-uc-lines">
214 {best.lines.map((l) => (
215 <li key={l.key} data-zero={l.amount === 0 ? "" : undefined}>
216 <i style={{ background: COLORS[l.key] }} aria-hidden="true" />
217 <span className="fd-uc-l">
218 {l.label}
219 <small>{l.detail}</small>
220 </span>
221 <span className="fd-uc-amt">{money(l.amount)}</span>
222 </li>
223 ))}
224 </ul>
225 <div className="fd-uc-bar" aria-hidden="true">
226 {best.lines.map((l) => (
227 <span key={l.key} style={{ flexGrow: Math.max(l.amount, 0.0001), background: COLORS[l.key] }} />
228 ))}
229 </div>
230 <div className="fd-uc-total">
231 <span>Estimated / month</span>
232 <strong aria-hidden="true">
233 {whole}
234 <small>.{cents}</small>
235 </strong>
236 </div>
237 <div className="fd-uc-cmp" aria-label="Same usage on each plan">
238 {priced.map((p) => {
239 const max = Math.max(...priced.filter((q) => q.ok).map((q) => q.total), 1);
240 return (
241 <div key={p.plan.id} className="fd-uc-cmp-row" data-best={p.plan.id === best.plan.id ? "" : undefined}>
242 <span>{p.plan.name}</span>
243 <span className="fd-uc-cmp-bar" aria-hidden="true">
244 <span style={{ transform: `scaleX(${p.ok ? Math.max(p.total / max, 0.01) : 0})` }} />
245 </span>
246 <span className="fd-uc-cmp-v">{p.ok ? money(p.total, 0) : "over limit"}</span>
247 </div>
248 );
249 })}
250 </div>
251 <a className="fd-uc-cta" href="#signup">
252 Deploy on {best.plan.name}
253 <span aria-hidden="true">↵</span>
254 </a>
255 </>
256 ) : (
257 <div className="fd-uc-ent">
258 <p>Past 1B requests or 40 seats we write a contract: committed-use discounts, a 99.99% SLA and a named on-call engineer.</p>
259 <a className="fd-uc-cta" href="#sales">
260 Talk to infrastructure sales
261 <span aria-hidden="true">↵</span>
262 </a>
263 </div>
264 )}
265 <p className="fd-uc-note">Prices in USD, excl. tax. Egress is free up to 1 TB per 100M requests.</p>
266 </div>
267 </div>
268 <div className="fd-uc-sr" role="status" aria-live="polite">{live}</div>
269 </section>
270 );
271}
272
273const css = `
274:where(.fd-uc,.fd-uc *){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}
275:where(.fd-uc,.fd-uc *)::before,:where(.fd-uc,.fd-uc *)::after{box-sizing:border-box}
276.fd-uc{
277 --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);
278 --fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;--fd-dur-4:700ms;
279 --fd-uc-bg:#0D0D0B;--fd-uc-fg:#F2EFE6;--fd-uc-signal:#FFB224;
280 --mut:#8E8A7E;--dim:#5A574F;--line:rgba(242,239,230,.1);--panel:#151512;
281 --sans:ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;
282 --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;
283 display:block;width:100%;background:var(--fd-uc-bg);color:var(--fd-uc-fg);font:400 15px/22px var(--sans);-webkit-font-smoothing:antialiased;
284 padding:clamp(48px,7vw,96px) clamp(16px,4vw,56px)}
285.fd-uc-wrap{max-width:1200px;margin:0 auto;display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);gap:clamp(32px,6vw,96px);align-items:start}
286.fd-uc-eyebrow{font:400 13px/20px var(--mono);color:var(--fd-uc-signal)}
287.fd-uc-eyebrow span::after{content:"";display:inline-block;width:8px;height:15px;margin-left:4px;vertical-align:-3px;background:var(--fd-uc-signal);animation:fd-uc-blink 1.1s steps(1) infinite}
288@keyframes fd-uc-blink{50%{opacity:0}}
289.fd-uc-title{margin-top:20px;max-width:16ch;font:600 clamp(2rem,4.2vw,3.5rem)/1.02 var(--sans);letter-spacing:-.04em}
290.fd-uc-lede{margin-top:20px;max-width:52ch;color:var(--mut)}
291.fd-uc-sliders{margin-top:48px;display:grid;gap:36px}
292.fd-uc-sl-top{display:flex;align-items:baseline;justify-content:space-between;gap:16px}
293.fd-uc-sl-top label{font:500 13px/20px var(--mono);color:var(--mut);text-transform:uppercase;letter-spacing:.04em}
294.fd-uc-sl-top output{font:500 1.5rem/32px var(--mono);letter-spacing:-.03em;font-variant-numeric:tabular-nums;color:var(--fd-uc-fg)}
295.fd-uc-sl-top small{margin-left:6px;font-size:.8125rem;color:var(--mut);letter-spacing:0}
296.fd-uc-track{position:relative;margin-top:12px;height:44px}
297.fd-uc-track::before,.fd-uc-track::after{content:"";position:absolute;left:0;top:13px;height:2px;border-radius:2px}
298.fd-uc-track::before{right:0;background:var(--line)}
299.fd-uc-track::after{width:var(--p);background:var(--fd-uc-signal);transition:width var(--fd-dur-2) var(--fd-ease-out)}
300.fd-uc-track input{position:absolute;inset:0 -11px auto;width:calc(100% + 22px);height:28px;margin:0;-webkit-appearance:none;appearance:none;background:transparent;cursor:pointer;z-index:2}
301.fd-uc-track input::-webkit-slider-runnable-track{height:28px;background:transparent}
302.fd-uc-track input::-moz-range-track{height:28px;background:transparent}
303.fd-uc-track input::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:22px;height:22px;margin-top:3px;border-radius:4px;background:var(--fd-uc-fg);border:5px solid var(--fd-uc-bg);box-shadow:0 0 0 1.5px var(--fd-uc-fg);transition:transform var(--fd-dur-2) var(--fd-spring)}
304.fd-uc-track input::-moz-range-thumb{width:12px;height:12px;border-radius:4px;background:var(--fd-uc-fg);border:5px solid var(--fd-uc-bg);box-shadow:0 0 0 1.5px var(--fd-uc-fg)}
305.fd-uc-track input:active::-webkit-slider-thumb{transform:scale(1.18)}
306.fd-uc-track input:focus-visible{outline:none}
307.fd-uc-track input:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 1.5px var(--fd-uc-fg),0 0 0 4px color-mix(in srgb,var(--fd-uc-signal) 70%,transparent)}
308.fd-uc-track input:focus-visible::-moz-range-thumb{box-shadow:0 0 0 1.5px var(--fd-uc-fg),0 0 0 4px color-mix(in srgb,var(--fd-uc-signal) 70%,transparent)}
309.fd-uc-ticks{position:absolute;left:0;right:0;top:20px;height:24px}
310.fd-uc-ticks span{position:absolute;top:0;width:1px;height:5px;background:var(--dim)}
311.fd-uc-ticks span[data-on]{background:var(--fd-uc-signal)}
312.fd-uc-ticks em{position:absolute;top:8px;left:0;transform:translateX(-50%);font:400 11px/14px var(--mono);font-style:normal;color:var(--dim);white-space:nowrap}
313.fd-uc-ticks span:first-child em{transform:none}
314.fd-uc-ticks span:last-child em{transform:translateX(-100%)}
315.fd-uc-ticks span:not([data-on]):not(:first-child){opacity:.6}
316
317.fd-uc-bill{position:relative;padding:28px;border-radius:20px;background:var(--panel);box-shadow:inset 0 0 0 1px var(--line);font-family:var(--mono)}
318.fd-uc-detect{display:flex;align-items:center;flex-wrap:wrap;gap:10px 12px;padding-bottom:20px;border-bottom:1px dashed var(--line)}
319.fd-uc-led{display:block;width:8px;height:8px;border-radius:50%;background:var(--fd-uc-signal);box-shadow:0 0 0 3px color-mix(in srgb,var(--fd-uc-signal) 22%,transparent)}
320.fd-uc-detect-t{font:400 15px/22px var(--mono)}
321.fd-uc-detect-t b{display:inline-block;font-weight:700;color:var(--fd-uc-signal);animation:fd-uc-flip var(--fd-dur-3) var(--fd-spring)}
322@keyframes fd-uc-flip{from{transform:translateY(60%) rotateX(80deg);opacity:0}to{transform:none;opacity:1}}
323.fd-uc-saves{margin-left:auto;padding:3px 8px;border-radius:6px;font:500 11px/16px var(--mono);color:#0D0D0B;background:#5ED1A0}
324.fd-uc-lines{margin-top:8px}
325.fd-uc-lines li{display:grid;grid-template-columns:8px minmax(0,1fr) auto;align-items:start;gap:12px;padding:12px 0;border-bottom:1px solid var(--line);transition:opacity var(--fd-dur-2)}
326.fd-uc-lines li[data-zero]{opacity:.45}
327.fd-uc-lines i{display:block;width:8px;height:8px;margin-top:6px;border-radius:2px}
328.fd-uc-l{display:flex;flex-direction:column;font:500 13px/20px var(--mono)}
329.fd-uc-l small{font-size:11px;line-height:16px;color:var(--mut)}
330.fd-uc-amt{font:500 13px/20px var(--mono);font-variant-numeric:tabular-nums}
331.fd-uc-bar{display:flex;gap:2px;height:10px;margin-top:20px;border-radius:3px;overflow:hidden}
332.fd-uc-bar span{display:block;flex-basis:0;min-width:2px;transition:flex-grow var(--fd-dur-3) var(--fd-ease-out)}
333.fd-uc-total{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-top:20px}
334.fd-uc-total span{font:400 12px/18px var(--mono);color:var(--mut);text-transform:uppercase;letter-spacing:.04em;padding-bottom:8px}
335.fd-uc-total strong{font:600 clamp(2.5rem,4vw,3.25rem)/1 var(--mono);letter-spacing:-.05em;font-variant-numeric:tabular-nums}
336.fd-uc-total small{font-size:.45em;color:var(--mut);letter-spacing:-.02em}
337.fd-uc-cmp{display:grid;gap:6px;margin-top:24px;padding-top:16px;border-top:1px dashed var(--line)}
338.fd-uc-cmp-row{display:grid;grid-template-columns:52px minmax(0,1fr) 84px;align-items:center;gap:10px;font:400 12px/18px var(--mono);color:var(--mut)}
339.fd-uc-cmp-row[data-best]{color:var(--fd-uc-fg)}
340.fd-uc-cmp-bar{display:block;height:6px;border-radius:2px;background:var(--line);overflow:hidden}
341.fd-uc-cmp-bar span{display:block;height:100%;width:100%;transform-origin:left;background:var(--dim);transition:transform var(--fd-dur-3) var(--fd-ease-out)}
342.fd-uc-cmp-row[data-best] .fd-uc-cmp-bar span{background:var(--fd-uc-signal)}
343.fd-uc-cmp-v{text-align:right;font-variant-numeric:tabular-nums}
344.fd-uc-cta{display:flex;align-items:center;justify-content:space-between;margin-top:24px;height:48px;padding:0 18px;border-radius:10px;background:var(--fd-uc-signal);color:#0D0D0B;font:600 14px/48px var(--mono);transition:transform var(--fd-dur-2) var(--fd-spring),background-color var(--fd-dur-1)}
345.fd-uc-cta:hover{background:#FFC34D}
346.fd-uc-cta:active{transform:scale(.96);transition-duration:var(--fd-dur-1)}
347.fd-uc-cta:focus-visible{outline:2px solid color-mix(in srgb,var(--fd-uc-signal) 70%,transparent);outline-offset:2px}
348.fd-uc-ent{padding-top:20px}
349.fd-uc-ent p{font:400 14px/22px var(--mono);color:var(--mut)}
350.fd-uc-note{margin-top:16px;font:400 11px/16px var(--mono);color:var(--dim)}
351.fd-uc-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
352@media (max-width:880px){.fd-uc-wrap{grid-template-columns:1fr}.fd-uc-bill{padding:20px}}
353@media (prefers-reduced-motion:reduce){.fd-uc *{transition-duration:1ms!important;animation:none!important}}
354@media (prefers-contrast:more){.fd-uc{--line:rgba(242,239,230,.5)}}
355`;
356
357export function Demo() {
358 return (
359 <div className="fd-ucd">
360 <style>{demoCss}</style>
361 <header className="fd-ucd-nav">
362 <a href="#home" className="fd-ucd-logo">
363 <svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
364 <path d="M1 17L9 1l8 16H11.5L9 11.5 6.5 17z" fill="#FFB224" />
365 </svg>
366 kestrel<span>/edge</span>
367 </a>
368 <nav aria-label="Main">
369 <a href="#docs">Docs</a>
370 <a href="#pricing" aria-current="page">Pricing</a>
371 <a href="#status">Status: all systems normal</a>
372 </nav>
373 </header>
374 <UsageCalculator />
375 </div>
376 );
377}
378
379const demoCss = `
380:where(.fd-ucd,.fd-ucd *){box-sizing:border-box;margin:0;padding:0;font:inherit;color:inherit;text-decoration:none}
381.fd-ucd{min-height:100vh;background:#0D0D0B;color:#F2EFE6;font:400 13px/20px ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace}
382.fd-ucd-nav{display:flex;align-items:center;justify-content:space-between;gap:20px;height:56px;padding:0 clamp(16px,4vw,56px);border-bottom:1px solid rgba(242,239,230,.08)}
383.fd-ucd-logo{display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px;letter-spacing:-.02em}
384.fd-ucd-logo svg{display:block}
385.fd-ucd-logo span{color:#8E8A7E;font-weight:400}
386.fd-ucd-nav nav{display:flex;gap:24px;color:#8E8A7E}
387.fd-ucd-nav nav a[aria-current]{color:#F2EFE6}
388.fd-ucd-nav nav a:last-child{color:#5ED1A0}
389@media (max-width:640px){.fd-ucd-nav nav a:not([aria-current]){display:none}}
390`;

More Pricing. Same file contract.

Pricing Tiers Toggle, pricing preview: React pricing section with three plans, a monthly and yearly toggle, odometer prices and a set-apart…

Pricing Tiers Toggle

React pricing section with three plans, a monthly and yearly toggle, odometer prices and a set-apart recommended plan.

Membership Card, pricing preview: Single plan pricing as a membership card: spring 3D tilt, flips over, with honest included and excluded lists

Membership Card

Single plan pricing as a membership card: spring 3D tilt, flips over, with honest included and excluded lists.

Menu Pricing, pricing preview: Pricing section styled as a printed menu for a coffee subscription: dotted leaders, size columns and a…

Menu Pricing

Pricing section styled as a printed menu for a coffee subscription: dotted leaders, size columns and a pen-circled pick.

Questions about Usage Calculator

Run npx shadcn@latest add https://freedesignmd.com/r/usage-calculator.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.

Usage-based pricing calculator: snapping sliders price every plan at once, flag the cheapest and animate an itemised bill. 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. Styled native range inputs (WebKit and Gecko thumb selectors).

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.