Button Core

The complete button system: 5 variants, 3 sizes, icon slots, loading → success morph and a springy press.

  • Buttons
  • Fintech
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers. Uses color-mix() only in the demo.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/button-core.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/buttons/button-core.tsx
1/**
2 * ButtonCore — the full button system.
3 *
4 * Variants: primary · secondary · tertiary · ghost · destructive
5 * Sizes: sm (32) · md (40) · lg (48)
6 * Icons: iconLeft · iconRight · icon-only (pass `icon` + `aria-label`)
7 * States: hover · press (scale .96 + spring release) · focus-visible ·
8 * disabled · loading → success check → idle
9 *
10 * Props: variant, size, shape ("pill" | "soft"), iconLeft, iconRight, icon,
11 * loading (controlled), loadingLabel, successLabel, successDuration (ms),
12 * theme ("light" | "dark", otherwise inherits from the nearest
13 * [data-fd-theme] ancestor). `onClick` may return a Promise: the button then
14 * morphs label → spinner → check → label by itself.
15 * Wrap many buttons in <ButtonScope> to render the stylesheet only once.
16 *
17 * Browser support: all evergreen browsers. Needs React 18+ (SSR safe).
18 * Accessibility: native <button>; aria-busy while loading, a polite live
19 * region announces the success label; focus ring is never removed; while busy
20 * the button stays focusable (aria-disabled) so focus is not lost.
21 * Reduced motion: no scale/spring, spinner becomes a gentle pulse.
22 */
23import { createContext, forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react";
24import type { ButtonHTMLAttributes, MouseEvent, ReactNode } from "react";
25
26export type ButtonVariant = "primary" | "secondary" | "tertiary" | "ghost" | "destructive";
27export type ButtonSize = "sm" | "md" | "lg";
28type Phase = "idle" | "loading" | "success";
29
30export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
31 variant?: ButtonVariant;
32 size?: ButtonSize;
33 shape?: "pill" | "soft";
34 iconLeft?: ReactNode;
35 iconRight?: ReactNode;
36 /** Icon-only button. Always pair with aria-label. */
37 icon?: ReactNode;
38 loading?: boolean;
39 loadingLabel?: ReactNode;
40 successLabel?: ReactNode;
41 successDuration?: number;
42 theme?: "light" | "dark";
43 onClick?: (e: MouseEvent<HTMLButtonElement>) => void | Promise<unknown>;
44}
45
46const StyleCtx = createContext(false);
47
48/** Renders the stylesheet once for every Button inside it. */
49export function ButtonScope({ children }: { children: ReactNode }) {
50 return (
51 <StyleCtx.Provider value>
52 <style>{css}</style>
53 {children}
54 </StyleCtx.Provider>
55 );
56}
57
58export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
59 {
60 variant = "primary",
61 size = "md",
62 shape = "pill",
63 iconLeft,
64 iconRight,
65 icon,
66 loading,
67 loadingLabel,
68 successLabel,
69 successDuration = 1600,
70 theme,
71 onClick,
72 children,
73 className,
74 type = "button",
75 ...rest
76 },
77 ref,
78) {
79 const scoped = useContext(StyleCtx);
80 const [phase, setPhase] = useState<Phase>("idle");
81 const alive = useRef(true);
82 useEffect(() => {
83 alive.current = true;
84 return () => {
85 alive.current = false;
86 };
87 }, []);
88
89 const shown: Phase = loading ? "loading" : phase;
90 const busy = shown !== "idle";
91
92 const handle = useCallback(
93 async (e: MouseEvent<HTMLButtonElement>) => {
94 if (busy || rest.disabled) {
95 e.preventDefault();
96 return;
97 }
98 const out = onClick?.(e);
99 if (out && typeof (out as Promise<unknown>).then === "function") {
100 setPhase("loading");
101 try {
102 await out;
103 if (!alive.current) return;
104 setPhase("success");
105 window.setTimeout(() => alive.current && setPhase("idle"), successDuration);
106 } catch {
107 if (alive.current) setPhase("idle");
108 }
109 }
110 },
111 [busy, onClick, rest.disabled, successDuration],
112 );
113
114 return (
115 <>
116 {!scoped && <style>{css}</style>}
117 <button
118 ref={ref}
119 type={type}
120 className={`fd-btn${className ? " " + className : ""}`}
121 data-variant={variant}
122 data-size={size}
123 data-shape={shape}
124 data-phase={shown}
125 data-icon-only={icon ? "" : undefined}
126 data-fd-theme={theme}
127 aria-busy={shown === "loading" || undefined}
128 aria-disabled={busy || undefined}
129 onClick={handle}
130 {...rest}
131 >
132 <span className="fd-btn-layer" data-layer="idle">
133 {icon ? (
134 <span className="fd-btn-icon">{icon}</span>
135 ) : (
136 <>
137 {iconLeft && <span className="fd-btn-icon">{iconLeft}</span>}
138 <span className="fd-btn-label">{children}</span>
139 {iconRight && <span className="fd-btn-icon">{iconRight}</span>}
140 </>
141 )}
142 </span>
143 <span className="fd-btn-layer" data-layer="loading" aria-hidden="true">
144 <Spinner />
145 {loadingLabel && !icon && <span className="fd-btn-label">{loadingLabel}</span>}
146 </span>
147 <span className="fd-btn-layer" data-layer="success" aria-hidden="true">
148 <Check />
149 {successLabel && !icon && <span className="fd-btn-label">{successLabel}</span>}
150 </span>
151 <span className="fd-btn-sr" role="status" aria-live="polite">
152 {shown === "success" ? (typeof successLabel === "string" ? successLabel : "Done") : ""}
153 </span>
154 </button>
155 </>
156 );
157});
158
159function Spinner() {
160 return (
161 <svg className="fd-btn-spin" width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
162 <circle cx="12" cy="12" r="9" stroke="currentColor" strokeOpacity=".25" strokeWidth="2.5" />
163 <path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
164 </svg>
165 );
166}
167
168function Check() {
169 return (
170 <svg className="fd-btn-check" width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
171 <path d="M5 12.5l4.5 4.5L19 7.5" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
172 </svg>
173 );
174}
175
176/* ───────────────────────────── styles ───────────────────────────── */
177
178const css = `
179:where(.fd-btn,.fd-btn *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
180:where(.fd-btn,.fd-btn *)::before,:where(.fd-btn,.fd-btn *)::after{box-sizing:border-box}
181:where(.fd-btn svg){display:block;flex-shrink:0}
182.fd-btn{
183 --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);
184 --fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;
185 --b-accent:#007AFF;--b-accent-hover:#1A8CFF;--b-accent-soft:rgba(0,122,255,.1);--b-accent-soft-hover:rgba(0,122,255,.16);--b-on-accent:#fff;
186 --b-danger:#E5334B;--b-danger-hover:#EF4A60;--b-success:#1FA45B;
187 --b-surface:#fff;--b-surface-hover:#F7F7F9;--b-border:rgba(11,11,15,.12);--b-ink:#0B0B0F;--b-ghost-hover:rgba(11,11,15,.06);
188 --b-shadow:0 1px 2px rgba(11,11,15,.06),0 1px 1px rgba(11,11,15,.04);
189 --b-inset:0 1px 0 rgba(255,255,255,.22) inset;
190 --b-ring:rgba(0,122,255,.7);
191 position:relative;display:inline-grid;place-items:center;flex-shrink:0;
192 appearance:none;-webkit-appearance:none;border:0;outline:0;cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent;
193 font:500 15px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:-.005em;
194 height:var(--b-h);min-width:var(--b-h);padding:0 var(--b-px);border-radius:var(--b-r);
195 background:var(--b-bg);color:var(--b-fg);box-shadow:var(--b-sh);
196 transform:scale(1);
197 transition:transform var(--fd-dur-3) var(--fd-spring),background-color var(--fd-dur-1) var(--fd-ease-out),box-shadow var(--fd-dur-2) var(--fd-ease-out),color var(--fd-dur-2) var(--fd-ease-out);
198 white-space:nowrap;
199}
200[data-fd-theme="dark"] .fd-btn,.fd-btn[data-fd-theme="dark"]{
201 --b-accent:#0A84FF;--b-accent-hover:#3B9CFF;--b-accent-soft:rgba(10,132,255,.16);--b-accent-soft-hover:rgba(10,132,255,.24);
202 --b-danger:#FF453A;--b-danger-hover:#FF6A60;--b-success:#30D158;
203 --b-surface:#1C1C22;--b-surface-hover:#25252C;--b-border:rgba(255,255,255,.1);--b-ink:#F5F5F7;--b-ghost-hover:rgba(255,255,255,.08);
204 --b-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 1px 2px rgba(0,0,0,.3);
205 --b-inset:0 1px 0 rgba(255,255,255,.28) inset;--b-ring:rgba(10,132,255,.8);
206}
207.fd-btn[data-fd-theme="light"]{--b-accent:#007AFF;--b-surface:#fff;--b-border:rgba(11,11,15,.12);--b-ink:#0B0B0F}
208.fd-btn[data-size="sm"]{--b-h:32px;--b-px:12px;--b-gap:6px;--b-fs:13px;--b-ic:16px}
209.fd-btn[data-size="md"]{--b-h:40px;--b-px:16px;--b-gap:8px;--b-fs:15px;--b-ic:18px}
210.fd-btn[data-size="lg"]{--b-h:48px;--b-px:22px;--b-gap:10px;--b-fs:17px;--b-ic:20px;letter-spacing:-.01em}
211.fd-btn[data-size="sm"]{font-size:13px}.fd-btn[data-size="md"]{font-size:15px}.fd-btn[data-size="lg"]{font-size:17px}
212.fd-btn[data-shape="pill"]{--b-r:9999px}
213.fd-btn[data-shape="soft"][data-size="sm"]{--b-r:8px}.fd-btn[data-shape="soft"][data-size="md"]{--b-r:10px}.fd-btn[data-shape="soft"][data-size="lg"]{--b-r:12px}
214.fd-btn[data-icon-only]{padding:0}
215
216.fd-btn[data-variant="primary"]{--b-bg:var(--b-accent);--b-fg:var(--b-on-accent);--b-sh:var(--b-inset),0 1px 2px rgba(0,40,120,.35),0 8px 18px -10px var(--b-accent)}
217.fd-btn[data-variant="secondary"]{--b-bg:var(--b-surface);--b-fg:var(--b-ink);--b-sh:0 0 0 1px var(--b-border),var(--b-shadow)}
218.fd-btn[data-variant="tertiary"]{--b-bg:var(--b-accent-soft);--b-fg:var(--b-accent);--b-sh:none}
219.fd-btn[data-variant="ghost"]{--b-bg:transparent;--b-fg:var(--b-ink);--b-sh:none}
220.fd-btn[data-variant="destructive"]{--b-bg:var(--b-danger);--b-fg:#fff;--b-sh:var(--b-inset),0 1px 2px rgba(120,0,20,.35),0 8px 18px -10px var(--b-danger)}
221
222@media (hover:hover){
223 .fd-btn[data-variant="primary"]:hover{--b-bg:var(--b-accent-hover)}
224 .fd-btn[data-variant="secondary"]:hover{--b-bg:var(--b-surface-hover)}
225 .fd-btn[data-variant="tertiary"]:hover{--b-bg:var(--b-accent-soft-hover)}
226 .fd-btn[data-variant="ghost"]:hover{--b-bg:var(--b-ghost-hover)}
227 .fd-btn[data-variant="destructive"]:hover{--b-bg:var(--b-danger-hover)}
228}
229.fd-btn:active{transform:scale(.96);transition:transform var(--fd-dur-1) var(--fd-ease-out),background-color var(--fd-dur-1) var(--fd-ease-out)}
230.fd-btn:focus-visible{outline:2px solid var(--b-ring);outline-offset:2px}
231.fd-btn:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
232.fd-btn:disabled:hover{--b-bg:var(--b-bg)}
233.fd-btn[aria-disabled="true"]{cursor:default}
234.fd-btn[data-phase="success"][data-variant="primary"],.fd-btn[data-phase="success"][data-variant="destructive"]{--b-bg:var(--b-success);--b-sh:var(--b-inset),0 1px 2px rgba(0,60,20,.35),0 8px 18px -10px var(--b-success)}
235.fd-btn[data-phase="success"][data-variant="secondary"],.fd-btn[data-phase="success"][data-variant="tertiary"],.fd-btn[data-phase="success"][data-variant="ghost"]{--b-fg:var(--b-success)}
236
237.fd-btn-layer{grid-area:1/1;display:inline-flex;align-items:center;justify-content:center;gap:var(--b-gap);
238 transition:opacity var(--fd-dur-2) var(--fd-ease-in-out),transform var(--fd-dur-3) var(--fd-ease-out);}
239.fd-btn-layer[data-layer="idle"]{opacity:1;transform:none}
240.fd-btn-layer[data-layer="loading"],.fd-btn-layer[data-layer="success"]{opacity:0;transform:translateY(8px) scale(.85);pointer-events:none}
241.fd-btn[data-phase="loading"] .fd-btn-layer[data-layer="idle"],.fd-btn[data-phase="success"] .fd-btn-layer[data-layer="idle"]{opacity:0;transform:translateY(-8px) scale(.9)}
242.fd-btn[data-phase="loading"] .fd-btn-layer[data-layer="loading"],.fd-btn[data-phase="success"] .fd-btn-layer[data-layer="success"]{opacity:1;transform:none}
243.fd-btn-label{display:block;line-height:1}
244.fd-btn-icon{display:grid;place-items:center;width:var(--b-ic);height:var(--b-ic)}
245.fd-btn-icon svg{width:100%;height:100%}
246.fd-btn-spin,.fd-btn-check{width:var(--b-ic);height:var(--b-ic)}
247.fd-btn-spin{animation:fd-btn-rot 800ms linear infinite}
248.fd-btn-check path{stroke-dasharray:24;stroke-dashoffset:24}
249.fd-btn[data-phase="success"] .fd-btn-check path{animation:fd-btn-draw 380ms var(--fd-ease-out) 120ms forwards}
250.fd-btn-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
251@keyframes fd-btn-rot{to{transform:rotate(360deg)}}
252@keyframes fd-btn-draw{to{stroke-dashoffset:0}}
253@media (prefers-reduced-motion:reduce){
254 .fd-btn,.fd-btn:active{transition:background-color var(--fd-dur-1) linear;transform:none}
255 .fd-btn-layer{transition:opacity 100ms linear;transform:none!important}
256 .fd-btn-spin{animation:fd-btn-pulse 1.4s ease-in-out infinite}
257 .fd-btn[data-phase="success"] .fd-btn-check path{animation:none;stroke-dashoffset:0}
258}
259@keyframes fd-btn-pulse{50%{opacity:.35}}
260@media (prefers-contrast:more){.fd-btn[data-variant="tertiary"],.fd-btn[data-variant="ghost"]{box-shadow:0 0 0 1px currentColor}}
261`;
262
263/* ───────────────────────────── demo ───────────────────────────── */
264
265const I = {
266 lock: (
267 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="11" width="14" height="9" rx="2.5" /><path d="M8 11V8a4 4 0 0 1 8 0v3" /></svg>
268 ),
269 arrow: (
270 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
271 ),
272 plus: (
273 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg>
274 ),
275 trash: (
276 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3" /></svg>
277 ),
278 share: (
279 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 4v12M7 9l5-5 5 5M5 14v4a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-4" /></svg>
280 ),
281 card: (
282 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="5" width="18" height="14" rx="3" /><path d="M3 10h18M7 15h3" /></svg>
283 ),
284 bank: (
285 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 10l9-6 9 6M5 10v8M9.5 10v8M14.5 10v8M19 10v8M3 20h18" /></svg>
286 ),
287 sun: (
288 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M5 19l1.5-1.5M17.5 6.5L19 5" /></svg>
289 ),
290 moon: (
291 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z" /></svg>
292 ),
293};
294
295const sizes: ButtonSize[] = ["sm", "md", "lg"];
296const variants: ButtonVariant[] = ["primary", "secondary", "tertiary", "ghost", "destructive"];
297
298export function Demo() {
299 const [theme, setTheme] = useState<"light" | "dark">("light");
300 const [method, setMethod] = useState<"card" | "bank">("card");
301 const wait = (ms: number) => new Promise((r) => window.setTimeout(r, ms));
302
303 return (
304 <div className="fd-btn-demo" data-fd-theme={theme}>
305 <style>{demoCss}</style>
306 <ButtonScope>
307 <header className="fd-btn-demo-bar">
308 <div className="fd-btn-demo-brand">
309 <span className="fd-btn-demo-logo" aria-hidden="true" />
310 Halden Pay
311 </div>
312 <Button
313 variant="ghost"
314 size="sm"
315 icon={theme === "light" ? I.moon : I.sun}
316 aria-label={theme === "light" ? "Switch to dark theme" : "Switch to light theme"}
317 onClick={() => setTheme(theme === "light" ? "dark" : "light")}
318 />
319 </header>
320
321 <main className="fd-btn-demo-main">
322 <section className="fd-btn-demo-card" aria-labelledby="fd-btn-pay-h">
323 <p className="fd-btn-demo-eyebrow">Invoice INV-2041 · due 4 Nov</p>
324 <h2 id="fd-btn-pay-h" className="fd-btn-demo-h">Pay Northwind Freight</h2>
325 <p className="fd-btn-demo-amount">$2,480<span>.00</span></p>
326
327 <dl className="fd-btn-demo-lines">
328 <div><dt>Pallet storage · Oct</dt><dd>$1,920.00</dd></div>
329 <div><dt>Cross-dock handling</dt><dd>$480.00</dd></div>
330 <div><dt>Processing fee</dt><dd>$80.00</dd></div>
331 </dl>
332
333 <div className="fd-btn-demo-methods" role="radiogroup" aria-label="Payment method">
334 {([
335 ["card", "Visa ending 4242", "Expires 08/28", I.card],
336 ["bank", "Chase checking ···· 0917", "ACH · free, 1–2 days", I.bank],
337 ] as const).map(([id, t, s, ic]) => (
338 <button key={id} type="button" role="radio" aria-checked={method === id} className="fd-btn-demo-method" onClick={() => setMethod(id)}>
339 <span className="fd-btn-demo-method-ic">{ic}</span>
340 <span className="fd-btn-demo-method-t">{t}<small>{s}</small></span>
341 <span className="fd-btn-demo-radio" aria-hidden="true" />
342 </button>
343 ))}
344 </div>
345
346 <Button
347 size="lg"
348 variant="primary"
349 iconLeft={I.lock}
350 loadingLabel="Processing"
351 successLabel="Payment sent"
352 onClick={() => wait(1800)}
353 style={{ width: "100%" }}
354 >
355 Pay $2,480.00
356 </Button>
357 <div className="fd-btn-demo-row">
358 <Button variant="secondary" size="md" style={{ flex: 1 }}>Save for later</Button>
359 <Button variant="ghost" size="md" style={{ flex: 1 }}>Cancel</Button>
360 </div>
361 <p className="fd-btn-demo-fine">Payments are encrypted. Funds settle in 1–2 business days.</p>
362 </section>
363
364 <section className="fd-btn-demo-card fd-btn-demo-spec" aria-labelledby="fd-btn-spec-h">
365 <p className="fd-btn-demo-eyebrow">Button system</p>
366 <h2 id="fd-btn-spec-h" className="fd-btn-demo-h">5 variants, 3 sizes</h2>
367
368 <div className="fd-btn-demo-matrix">
369 {variants.map((v) => (
370 <div className="fd-btn-demo-mrow" key={v}>
371 <span className="fd-btn-demo-mlabel">{v}</span>
372 <div className="fd-btn-demo-mcells">
373 {sizes.map((s) => (
374 <Button key={s} variant={v} size={s}>
375 {v === "destructive" ? "Delete" : v === "primary" ? "Continue" : "Label"}
376 </Button>
377 ))}
378 </div>
379 </div>
380 ))}
381 </div>
382
383 <div className="fd-btn-demo-divider" />
384 <div className="fd-btn-demo-mrow">
385 <span className="fd-btn-demo-mlabel">icons</span>
386 <div className="fd-btn-demo-mcells">
387 <Button variant="secondary" iconLeft={I.plus}>New invoice</Button>
388 <Button variant="primary" iconRight={I.arrow}>Next</Button>
389 <Button variant="secondary" icon={I.share} aria-label="Share" />
390 <Button variant="destructive" size="sm" icon={I.trash} aria-label="Delete" />
391 </div>
392 </div>
393 <div className="fd-btn-demo-mrow">
394 <span className="fd-btn-demo-mlabel">states</span>
395 <div className="fd-btn-demo-mcells">
396 <Button variant="primary" disabled>Disabled</Button>
397 <Button variant="secondary" loading loadingLabel="Loading" />
398 <Button variant="primary" shape="soft" onClick={() => wait(1200)} successLabel="Saved" loadingLabel="Saving">Save changes</Button>
399 </div>
400 </div>
401 </section>
402 </main>
403 </ButtonScope>
404 </div>
405 );
406}
407
408const demoCss = `
409:where(.fd-btn-demo,.fd-btn-demo *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
410:where(.fd-btn-demo,.fd-btn-demo *)::before,:where(.fd-btn-demo,.fd-btn-demo *)::after{box-sizing:border-box}
411:where(.fd-btn-demo svg){display:block;flex-shrink:0}
412:where(.fd-btn-demo button){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0}
413.fd-btn-demo{--d-font:ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;--d-bg:#F7F7F9;--d-card:#fff;--d-ink:#0B0B0F;--d-mut:#6B6B76;--d-line:rgba(11,11,15,.08);--d-soft:#F1F1F4;--d-accent:#007AFF;
414 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-dur-2:240ms;
415 min-height:100vh;background:var(--d-bg);color:var(--d-ink);font:400 15px/1.45 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;
416 -webkit-font-smoothing:antialiased;transition:background-color var(--fd-dur-2) var(--fd-ease-out)}
417.fd-btn-demo[data-fd-theme="dark"]{--d-bg:#0B0B0F;--d-card:#141418;--d-ink:#F5F5F7;--d-mut:#9A9AA6;--d-line:rgba(255,255,255,.08);--d-soft:#1C1C22;--d-accent:#0A84FF}
418.fd-btn-demo-bar{display:flex;align-items:center;justify-content:space-between;height:56px;padding:0 24px;border-bottom:1px solid var(--d-line)}
419.fd-btn-demo-brand{display:flex;align-items:center;gap:10px;font:600 15px/1 var(--d-font);letter-spacing:-.01em}
420.fd-btn-demo-logo{width:20px;height:20px;border-radius:6px;background:var(--d-ink);position:relative}
421.fd-btn-demo-logo::after{content:"";position:absolute;inset:6px;border-radius:2px;background:var(--d-bg)}
422.fd-btn-demo-main{display:grid;grid-template-columns:minmax(0,420px) minmax(0,520px);gap:24px;justify-content:center;align-items:start;padding:48px 24px 64px}
423.fd-btn-demo-card{display:flex;flex-direction:column;gap:16px;padding:28px;border-radius:24px;background:var(--d-card);
424 box-shadow:0 0 0 1px var(--d-line),0 1px 2px rgba(0,0,0,.04),0 24px 48px -24px rgba(0,0,0,.18)}
425.fd-btn-demo[data-fd-theme="dark"] .fd-btn-demo-card{box-shadow:0 0 0 1px var(--d-line),0 1px 0 rgba(255,255,255,.04) inset,0 24px 48px -24px rgba(0,0,0,.6)}
426.fd-btn-demo-eyebrow{font:500 13px/1 var(--d-font);color:var(--d-mut);letter-spacing:0}
427.fd-btn-demo-h{font:600 20px/1.2 var(--d-font);letter-spacing:-.01em}
428.fd-btn-demo-amount{font:600 44px/1 ui-sans-serif,-apple-system,"SF Pro Display","Inter",sans-serif;letter-spacing:-.03em;font-variant-numeric:tabular-nums;margin:4px 0 4px}
429.fd-btn-demo-amount span{color:var(--d-mut)}
430.fd-btn-demo-lines{display:flex;flex-direction:column;gap:12px;padding:16px 0;border-block:1px solid var(--d-line);font-size:15px}
431.fd-btn-demo-lines div{display:flex;justify-content:space-between;gap:16px}
432.fd-btn-demo-lines dt{color:var(--d-mut)}
433.fd-btn-demo-lines dd{font-variant-numeric:tabular-nums}
434.fd-btn-demo-methods{display:flex;flex-direction:column;gap:8px}
435.fd-btn-demo-method{display:flex;align-items:center;gap:12px;width:100%;padding:12px 14px;border-radius:14px;text-align:left;cursor:pointer;background:var(--d-card);
436 box-shadow:0 0 0 1px var(--d-line);transition:box-shadow var(--fd-dur-2) var(--fd-ease-out),background-color var(--fd-dur-2) var(--fd-ease-out);font:inherit;color:inherit}
437.fd-btn-demo-method:focus-visible{outline:2px solid color-mix(in srgb,var(--d-accent) 70%,transparent);outline-offset:2px}
438.fd-btn-demo-method[aria-checked="true"]{box-shadow:0 0 0 1.5px var(--d-accent);background:color-mix(in srgb,var(--d-accent) 5%,var(--d-card))}
439.fd-btn-demo-method-ic{display:grid;place-items:center;width:36px;height:36px;border-radius:10px;background:var(--d-soft)}
440.fd-btn-demo-method-ic svg{width:18px;height:18px}
441.fd-btn-demo-method-t{display:flex;flex-direction:column;gap:2px;flex:1;font:500 15px/1.2 var(--d-font)}
442.fd-btn-demo-method-t small{font:400 13px/1.2 var(--d-font);color:var(--d-mut)}
443.fd-btn-demo-radio{width:18px;height:18px;border-radius:50%;box-shadow:0 0 0 1.5px var(--d-line) inset,0 0 0 1px var(--d-mut) inset;opacity:.6;transition:all var(--fd-dur-2) var(--fd-ease-out)}
444.fd-btn-demo-method[aria-checked="true"] .fd-btn-demo-radio{opacity:1;box-shadow:0 0 0 5px var(--d-accent) inset}
445.fd-btn-demo-row{display:flex;gap:8px}
446.fd-btn-demo-fine{font:400 13px/1.4 var(--d-font);color:var(--d-mut);text-align:center}
447.fd-btn-demo-matrix{display:flex;flex-direction:column;gap:16px}
448.fd-btn-demo-mrow{display:grid;grid-template-columns:84px 1fr;align-items:center;gap:12px}
449.fd-btn-demo-mlabel{font:500 12px/1 ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;color:var(--d-mut)}
450.fd-btn-demo-mcells{display:flex;flex-wrap:wrap;align-items:center;gap:12px}
451.fd-btn-demo-divider{height:1px;background:var(--d-line);margin:4px 0}
452@media (max-width:900px){.fd-btn-demo-main{grid-template-columns:minmax(0,1fr);padding:24px 16px 48px}.fd-btn-demo-bar{padding:0 16px}}
453@media (max-width:520px){.fd-btn-demo-card{padding:20px;border-radius:20px}.fd-btn-demo-mrow{grid-template-columns:1fr;gap:8px}}
454@media (prefers-reduced-motion:reduce){.fd-btn-demo,.fd-btn-demo *{transition-duration:.01ms!important}}
455`;

More Buttons. Same file contract.

Shimmer Button

A dark capsule with a travelling light sweep and a rotating border glow for your one important call to action.

Magnetic Button

A button that leans toward your cursor on a spring, with the label drifting further than the shell.

Hold to Confirm

Press and hold to confirm dangerous actions. Progress fills through the button, early release cancels, Space works too.