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
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 snapping3 * 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 itemised5 * bill whose total counts to its new value. A stacked bar breaks the bill6 * down and a small comparison shows what the other plans would have cost.7 *8 * Props: plans (base, included quotas, overage rates, limits), defaults9 * ({ 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 ("40015 * million requests"); the total sits in a polite live region that updates16 * after the animation settles. Bars are aria-hidden; the itemised list carries17 * 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; // millions27 incStorage: number; // GB28 incSeats: number;29 perMillion: number | null; // null = hard limit30 perGB: number | null;31 perSeat: number | null;32 maxRequests: number; // millions33 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 <input123 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-deps172 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 <Slider190 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'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 sales261 <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.
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.


