Hold to Confirm

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

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

Browser support · All evergreen browsers. Uses pointer events and clip-path.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/hold-to-confirm.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/buttons/hold-to-confirm.tsx
1/**
2 * HoldToConfirm — press-and-hold to run a destructive or irreversible action.
3 * Progress fills through the button; releasing early drains it back and
4 * cancels. Works with pointer, touch and keyboard (hold Space or Enter).
5 *
6 * Props: onConfirm, duration (ms, default 1500), tone ("danger" | "neutral" |
7 * "accent"), size ("md" | "lg"), label, holdingLabel, doneLabel, resetAfter
8 * (ms before it returns to idle, default 2200; 0 = stay done), onProgress,
9 * onCancel, theme, disabled. CSS vars: --fd-hold-color, --fd-hold-radius.
10 *
11 * Browser support: all evergreen browsers (clip-path inset + CSS variables).
12 * Accessibility: native <button>; a polite live region announces start,
13 * cancel and completion; instructions are linked with aria-describedby.
14 * Holding via keyboard cancels on keyup or blur. Reduced motion: no scale or
15 * shake — the fill still shows progress.
16 */
17import { useCallback, useEffect, useId, useRef, useState } from "react";
18import type { ReactNode } from "react";
19
20export interface HoldToConfirmProps {
21 onConfirm?: () => void;
22 onCancel?: () => void;
23 onProgress?: (p: number) => void;
24 duration?: number;
25 tone?: "danger" | "neutral" | "accent";
26 size?: "md" | "lg";
27 label?: ReactNode;
28 holdingLabel?: ReactNode;
29 doneLabel?: ReactNode;
30 resetAfter?: number;
31 disabled?: boolean;
32 theme?: "light" | "dark";
33 icon?: ReactNode;
34 className?: string;
35}
36
37type Phase = "idle" | "holding" | "done";
38
39export function HoldToConfirm({
40 onConfirm,
41 onCancel,
42 onProgress,
43 duration = 1500,
44 tone = "danger",
45 size = "lg",
46 label = "Hold to confirm",
47 holdingLabel = "Keep holding…",
48 doneLabel = "Confirmed",
49 resetAfter = 2200,
50 disabled,
51 theme,
52 icon,
53 className,
54}: HoldToConfirmProps) {
55 const hintId = useId();
56 const btn = useRef<HTMLButtonElement>(null);
57 const [phase, setPhase] = useState<Phase>("idle");
58 const [msg, setMsg] = useState("");
59 const st = useRef({ p: 0, dir: 0, raf: 0, t0: 0, from: 0, phase: "idle" as Phase, key: false });
60 const cb = useRef({ onConfirm, onCancel, onProgress, duration, resetAfter });
61 cb.current = { onConfirm, onCancel, onProgress, duration, resetAfter };
62 const timer = useRef(0);
63
64 const paint = (p: number) => {
65 st.current.p = p;
66 btn.current?.style.setProperty("--h-p", p.toFixed(4));
67 cb.current.onProgress?.(p);
68 };
69 const setP = (ph: Phase) => {
70 st.current.phase = ph;
71 setPhase(ph);
72 };
73
74 const loop = useCallback((t: number) => {
75 const s = st.current;
76 if (s.dir === 1) {
77 const p = Math.min(1, s.from + (t - s.t0) / cb.current.duration);
78 paint(p);
79 if (p >= 1) {
80 s.dir = 0;
81 s.raf = 0;
82 setP("done");
83 setMsg("Confirmed");
84 cb.current.onConfirm?.();
85 if (cb.current.resetAfter > 0)
86 timer.current = window.setTimeout(() => {
87 paint(0);
88 setP("idle");
89 setMsg("");
90 }, cb.current.resetAfter);
91 return;
92 }
93 } else if (s.dir === -1) {
94 const k = Math.min(1, (t - s.t0) / (260 + 240 * s.from));
95 const p = s.from * (1 - (1 - Math.pow(1 - k, 3)));
96 paint(k >= 1 ? 0 : p);
97 if (k >= 1) {
98 s.dir = 0;
99 s.raf = 0;
100 return;
101 }
102 }
103 s.raf = requestAnimationFrame(loop);
104 }, []);
105
106 const start = () => {
107 const s = st.current;
108 if (disabled || s.phase === "done") return;
109 cancelAnimationFrame(s.raf);
110 s.dir = 1;
111 s.from = s.p;
112 s.t0 = performance.now();
113 setP("holding");
114 setMsg("Hold to confirm");
115 s.raf = requestAnimationFrame(loop);
116 };
117 const stop = () => {
118 const s = st.current;
119 if (s.phase !== "holding") return;
120 cancelAnimationFrame(s.raf);
121 s.dir = -1;
122 s.from = s.p;
123 s.t0 = performance.now();
124 setP("idle");
125 setMsg("Cancelled");
126 cb.current.onCancel?.();
127 s.raf = requestAnimationFrame(loop);
128 };
129
130 useEffect(
131 () => () => {
132 cancelAnimationFrame(st.current.raf);
133 window.clearTimeout(timer.current);
134 },
135 [],
136 );
137
138 const shown = phase === "done" ? doneLabel : phase === "holding" ? holdingLabel : label;
139
140 return (
141 <span className={`fd-hold${className ? " " + className : ""}`} data-fd-theme={theme} data-tone={tone} data-size={size} data-phase={phase}>
142 <style>{css}</style>
143 <button
144 ref={btn}
145 type="button"
146 className="fd-hold-btn"
147 disabled={disabled}
148 aria-describedby={hintId}
149 onPointerDown={(e) => {
150 if (e.button !== 0) return;
151 e.currentTarget.setPointerCapture?.(e.pointerId);
152 start();
153 }}
154 onPointerUp={stop}
155 onPointerCancel={stop}
156 onLostPointerCapture={stop}
157 onKeyDown={(e) => {
158 if ((e.key === " " || e.key === "Enter") && !e.repeat) {
159 e.preventDefault();
160 st.current.key = true;
161 start();
162 }
163 }}
164 onKeyUp={(e) => {
165 if ((e.key === " " || e.key === "Enter") && st.current.key) {
166 st.current.key = false;
167 stop();
168 }
169 }}
170 onBlur={() => {
171 st.current.key = false;
172 stop();
173 }}
174 onContextMenu={(e) => e.preventDefault()}
175 >
176 <span className="fd-hold-face" data-layer="base">
177 {icon && <span className="fd-hold-icon">{icon}</span>}
178 <span className="fd-hold-text">{shown}</span>
179 </span>
180 <span className="fd-hold-fill" aria-hidden="true">
181 <span className="fd-hold-face" data-layer="on">
182 {phase === "done" ? (
183 <svg className="fd-hold-check" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg>
184 ) : (
185 icon && <span className="fd-hold-icon">{icon}</span>
186 )}
187 <span className="fd-hold-text">{shown}</span>
188 </span>
189 </span>
190 </button>
191 <span id={hintId} className="fd-hold-sr">
192 Press and hold for {(duration / 1000).toFixed(1)} seconds, or hold the Space key.
193 </span>
194 <span className="fd-hold-sr" role="status" aria-live="polite">{msg}</span>
195 </span>
196 );
197}
198
199const css = `
200:where(.fd-hold,.fd-hold *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
201:where(.fd-hold,.fd-hold *)::before,:where(.fd-hold,.fd-hold *)::after{box-sizing:border-box}
202:where(.fd-hold svg){display:block;flex-shrink:0}
203:where(.fd-hold button){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0}
204.fd-hold{
205 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-spring:cubic-bezier(.34,1.56,.64,1);--fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;
206 --h-c:#E5334B;--h-soft:rgba(229,51,75,.1);--h-fg:#B3162E;--h-on:#fff;--h-line:rgba(229,51,75,.28);
207 display:inline-flex;vertical-align:middle;position:relative;
208 font:500 15px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;
209}
210.fd-hold[data-tone="neutral"]{--h-c:#0B0B0F;--h-soft:rgba(11,11,15,.06);--h-fg:#0B0B0F;--h-on:#fff;--h-line:rgba(11,11,15,.14)}
211.fd-hold[data-tone="accent"]{--h-c:#007AFF;--h-soft:rgba(0,122,255,.1);--h-fg:#0058C7;--h-on:#fff;--h-line:rgba(0,122,255,.28)}
212[data-fd-theme="dark"] .fd-hold,.fd-hold[data-fd-theme="dark"]{--h-c:#FF453A;--h-soft:rgba(255,69,58,.14);--h-fg:#FF8F87;--h-on:#1A0504;--h-line:rgba(255,69,58,.32)}
213[data-fd-theme="dark"] .fd-hold[data-tone="neutral"],.fd-hold[data-tone="neutral"][data-fd-theme="dark"]{--h-c:#F5F5F7;--h-soft:rgba(255,255,255,.07);--h-fg:#F5F5F7;--h-on:#0B0B0F;--h-line:rgba(255,255,255,.14)}
214[data-fd-theme="dark"] .fd-hold[data-tone="accent"],.fd-hold[data-tone="accent"][data-fd-theme="dark"]{--h-c:#0A84FF;--h-soft:rgba(10,132,255,.16);--h-fg:#66B2FF;--h-on:#fff;--h-line:rgba(10,132,255,.36)}
215.fd-hold[data-phase="done"]{--h-c:#1FA45B;--h-soft:rgba(31,164,91,.12);--h-fg:#0F7A41;--h-on:#fff;--h-line:rgba(31,164,91,.3)}
216[data-fd-theme="dark"] .fd-hold[data-phase="done"],.fd-hold[data-fd-theme="dark"][data-phase="done"]{--h-c:#30D158;--h-soft:rgba(48,209,88,.14);--h-fg:#7BE69B;--h-on:#04210E;--h-line:rgba(48,209,88,.32)}
217.fd-hold[data-size="md"]{--h-h:40px;--h-px:18px;--h-fs:15px}
218.fd-hold[data-size="lg"]{--h-h:52px;--h-px:26px;--h-fs:15px}
219.fd-hold-btn{--h-p:0;position:relative;display:block;overflow:hidden;height:var(--h-h);min-width:176px;padding:0 var(--h-px);border-radius:var(--fd-hold-radius,9999px);
220 appearance:none;-webkit-appearance:none;border:0;outline:0;cursor:pointer;user-select:none;-webkit-user-select:none;touch-action:none;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;
221 background:var(--h-soft);color:var(--h-fg);font-weight:500;font-size:var(--h-fs);line-height:1;font-family:inherit;letter-spacing:-.005em;
222 box-shadow:0 0 0 1px var(--h-line),0 1px 2px rgba(11,11,15,.06);
223 transform:scale(1);transition:transform var(--fd-dur-3) var(--fd-spring),box-shadow var(--fd-dur-2) var(--fd-ease-out)}
224.fd-hold[data-phase="holding"] .fd-hold-btn{transform:scale(.97);transition:transform var(--fd-dur-1) var(--fd-ease-out);box-shadow:0 0 0 1px var(--h-c),0 6px 20px -8px var(--h-c)}
225.fd-hold-btn:focus-visible{outline:2px solid rgba(10,132,255,.8);outline-offset:2px}
226.fd-hold-btn:disabled{opacity:.4;cursor:not-allowed}
227.fd-hold-face{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:8px;padding:0 var(--h-px);white-space:nowrap}
228.fd-hold-fill{position:absolute;inset:0;background:var(--h-c);color:var(--h-on);
229 clip-path:inset(0 calc((1 - var(--h-p)) * 100% + 0px) 0 0);will-change:clip-path}
230.fd-hold[data-phase="done"] .fd-hold-fill{clip-path:inset(0)}
231.fd-hold-icon{display:grid;place-items:center;width:18px;height:18px}
232.fd-hold-icon svg{width:100%;height:100%}
233.fd-hold-text{display:block}
234.fd-hold-check path{stroke-dasharray:24;stroke-dashoffset:24;animation:fd-hold-draw 380ms var(--fd-ease-out) 80ms forwards}
235.fd-hold-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
236@keyframes fd-hold-draw{to{stroke-dashoffset:0}}
237@media (prefers-reduced-motion:reduce){.fd-hold-btn,.fd-hold[data-phase="holding"] .fd-hold-btn{transform:none;transition:none}.fd-hold-check path{animation:none;stroke-dashoffset:0}}
238@media (prefers-contrast:more){.fd-hold-btn{box-shadow:0 0 0 1px currentColor}}
239`;
240
241/* ───────────────────────────── demo ───────────────────────────── */
242
243const trash = (
244 <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>
245);
246const rocket = (
247 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 15c-1.5 1.3-2 5-2 5s3.7-.5 5-2M12 15l-3-3a22 22 0 0 1 2-4 12 12 0 0 1 11-5c0 3-1 8-5 11a22 22 0 0 1-4 2zM9 12H4s.6-3 2-4c1.6-1 5 0 5 0M12 15v5s3-.6 4-2c1-1.6 0-5 0-5" /></svg>
248);
249
250export function Demo() {
251 const [theme, setTheme] = useState<"light" | "dark">("dark");
252 const [deleted, setDeleted] = useState(false);
253 const [pct, setPct] = useState(0);
254 const [promoted, setPromoted] = useState(false);
255
256 return (
257 <div className="fd-hold-demo" data-fd-theme={theme}>
258 <style>{demoCss}</style>
259 <aside className="fd-hold-demo-side" aria-label="Project navigation">
260 <div className="fd-hold-demo-proj"><span className="fd-hold-demo-sq">O</span><div><b>orbit-api</b><small>Production</small></div></div>
261 <nav>
262 {["Overview", "Deployments", "Databases", "Logs", "Settings"].map((n) => (
263 <a key={n} href={`#${n}`} aria-current={n === "Databases" ? "page" : undefined}>{n}</a>
264 ))}
265 </nav>
266 </aside>
267
268 <main className="fd-hold-demo-main">
269 <div className="fd-hold-demo-top">
270 <div>
271 <p className="fd-hold-demo-crumb">orbit-api / Databases / orders-prod</p>
272 <h1>orders-prod</h1>
273 </div>
274 <button type="button" className="fd-hold-demo-theme" onClick={() => setTheme(theme === "dark" ? "light" : "dark")} aria-label={theme === "dark" ? "Switch to light theme" : "Switch to dark theme"}>
275 {theme === "dark" ? "Light" : "Dark"}
276 </button>
277 </div>
278
279 <section className="fd-hold-demo-stats" aria-label="Database summary">
280 <div><small>Engine</small><b>Postgres 16.4</b></div>
281 <div><small>Size</small><b>184.2 GB</b></div>
282 <div><small>Region</small><b>eu-central-1</b></div>
283 <div><small>Connections</small><b>212 / 500</b></div>
284 </section>
285
286 <section className="fd-hold-demo-card" aria-labelledby="fd-hold-promote">
287 <div>
288 <h2 id="fd-hold-promote">Promote read replica</h2>
289 <p>Switches writes to <code>orders-prod-replica-2</code>. Expect about 8 seconds of write downtime.</p>
290 </div>
291 <HoldToConfirm tone="accent" size="md" duration={1000} icon={rocket} label="Hold to promote" holdingLabel="Promoting…" doneLabel="Promoted" onConfirm={() => setPromoted(true)} resetAfter={2600} />
292 </section>
293
294 <section className="fd-hold-demo-card fd-hold-demo-danger" aria-labelledby="fd-hold-danger">
295 <div className="fd-hold-demo-danger-copy">
296 <span className="fd-hold-demo-badge">Danger zone</span>
297 <h2 id="fd-hold-danger">Delete production database</h2>
298 <p>This permanently removes <code>orders-prod</code>, its 3 replicas and 30 days of point-in-time backups. This cannot be undone.</p>
299 <ul>
300 <li>14 services will lose their connection string</li>
301 <li>Scheduled exports to s3://orbit-archive stop immediately</li>
302 </ul>
303 </div>
304 {deleted ? (
305 <div className="fd-hold-demo-gone" role="status">
306 <b>orders-prod deleted</b>
307 <span>Backups are kept for 7 days.</span>
308 <button type="button" onClick={() => { setDeleted(false); setPct(0); }}>Undo</button>
309 </div>
310 ) : (
311 <div className="fd-hold-demo-action">
312 <HoldToConfirm tone="danger" size="lg" icon={trash} duration={1800} label="Hold to delete" holdingLabel="Deleting…" doneLabel="Deleted" resetAfter={0} onProgress={setPct} onConfirm={() => setDeleted(true)} />
313 <small aria-hidden="true">{pct > 0 && pct < 1 ? `${Math.round(pct * 100)}%` : "Hold for 1.8s · or hold Space"}</small>
314 </div>
315 )}
316 </section>
317 {promoted && <p className="fd-hold-demo-note" role="status">Replica promoted. Writes now go to orders-prod-replica-2.</p>}
318 </main>
319 </div>
320 );
321}
322
323const demoCss = `
324:where(.fd-hold-demo,.fd-hold-demo *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
325:where(.fd-hold-demo,.fd-hold-demo *)::before,:where(.fd-hold-demo,.fd-hold-demo *)::after{box-sizing:border-box}
326:where(.fd-hold-demo button){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0}
327.fd-hold-demo{--d-font:ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;--d-mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;
328 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-dur-2:240ms;
329 --d-bg:#F7F7F9;--d-card:#fff;--d-side:#fff;--d-ink:#0B0B0F;--d-mut:#6B6B76;--d-line:rgba(11,11,15,.08);--d-code:rgba(11,11,15,.06);--d-danger:#E5334B;--d-dangerbg:rgba(229,51,75,.04);--d-dangerline:rgba(229,51,75,.22);
330 display:grid;grid-template-columns:232px minmax(0,1fr);min-height:100vh;background:var(--d-bg);color:var(--d-ink);font:400 15px/1.45 var(--d-font);-webkit-font-smoothing:antialiased;transition:background-color var(--fd-dur-2) var(--fd-ease-out)}
331.fd-hold-demo[data-fd-theme="dark"]{--d-bg:#09090B;--d-card:#101014;--d-side:#0B0B0F;--d-ink:#F5F5F7;--d-mut:#8E8E9A;--d-line:rgba(255,255,255,.08);--d-code:rgba(255,255,255,.08);--d-danger:#FF453A;--d-dangerbg:rgba(255,69,58,.05);--d-dangerline:rgba(255,69,58,.26)}
332.fd-hold-demo-side{display:flex;flex-direction:column;gap:20px;padding:20px 12px;background:var(--d-side);border-right:1px solid var(--d-line)}
333.fd-hold-demo-proj{display:flex;align-items:center;gap:10px;padding:0 8px}
334.fd-hold-demo-proj b{display:block;font:600 15px/1.1 var(--d-font);letter-spacing:-.01em}
335.fd-hold-demo-proj small{display:block;font:400 12px/1.4 var(--d-font);color:var(--d-mut)}
336.fd-hold-demo-sq{display:grid;place-items:center;width:32px;height:32px;border-radius:8px;background:var(--d-ink);color:var(--d-bg);font:600 14px/1 var(--d-font)}
337.fd-hold-demo-side nav{display:flex;flex-direction:column;gap:2px}
338.fd-hold-demo-side nav a{display:flex;align-items:center;height:32px;padding:0 10px;border-radius:8px;font:400 14px/1 var(--d-font);color:var(--d-mut)}
339.fd-hold-demo-side nav a:hover{background:var(--d-code);color:var(--d-ink)}
340.fd-hold-demo-side nav a[aria-current]{background:var(--d-code);color:var(--d-ink);font-weight:500}
341.fd-hold-demo-side a:focus-visible,.fd-hold-demo-theme:focus-visible,.fd-hold-demo-gone button:focus-visible{outline:2px solid rgba(10,132,255,.7);outline-offset:2px}
342.fd-hold-demo-main{display:flex;flex-direction:column;gap:16px;padding:32px clamp(16px,4vw,48px) 64px;max-width:1000px;width:100%}
343.fd-hold-demo-top{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:8px}
344.fd-hold-demo-crumb{font:400 13px/1 var(--d-mono);color:var(--d-mut);margin-bottom:12px}
345.fd-hold-demo-top h1{font:600 24px/1.15 var(--d-font);letter-spacing:-.02em}
346.fd-hold-demo-theme{cursor:pointer;height:32px;padding:0 14px;border-radius:9999px;box-shadow:0 0 0 1px var(--d-line);font:500 13px/1 var(--d-font);color:var(--d-mut)}
347.fd-hold-demo-theme:hover{color:var(--d-ink)}
348.fd-hold-demo-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border-radius:16px;background:var(--d-card);box-shadow:0 0 0 1px var(--d-line)}
349.fd-hold-demo-stats div{padding:16px 20px;display:flex;flex-direction:column;gap:6px;border-left:1px solid var(--d-line)}
350.fd-hold-demo-stats div:first-child{border-left:0}
351.fd-hold-demo-stats small{font:400 12px/1 var(--d-font);color:var(--d-mut)}
352.fd-hold-demo-stats b{font:500 15px/1.2 var(--d-font);font-variant-numeric:tabular-nums}
353.fd-hold-demo-card{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:24px;border-radius:20px;background:var(--d-card);box-shadow:0 0 0 1px var(--d-line),0 1px 2px rgba(0,0,0,.04),0 16px 32px -20px rgba(0,0,0,.2)}
354.fd-hold-demo-card h2{font:600 17px/1.2 var(--d-font);letter-spacing:-.01em;margin-bottom:6px}
355.fd-hold-demo-card p{font:400 14px/1.5 var(--d-font);color:var(--d-mut);max-width:52ch}
356.fd-hold-demo-card code{font:400 12.5px/1 var(--d-mono);padding:2px 6px;border-radius:6px;background:var(--d-code);color:var(--d-ink)}
357.fd-hold-demo-danger{align-items:flex-start;background:linear-gradient(0deg,var(--d-dangerbg),var(--d-dangerbg)),var(--d-card);box-shadow:0 0 0 1px var(--d-dangerline),0 16px 32px -20px rgba(0,0,0,.2)}
358.fd-hold-demo-badge{display:inline-flex;height:22px;align-items:center;padding:0 8px;border-radius:9999px;margin-bottom:12px;background:var(--d-dangerbg);box-shadow:0 0 0 1px var(--d-dangerline);font:500 12px/1 var(--d-font);color:var(--d-danger)}
359.fd-hold-demo-danger ul{margin-top:12px;display:flex;flex-direction:column;gap:6px;font:400 14px/1.4 var(--d-font);color:var(--d-mut)}
360.fd-hold-demo-danger li{display:flex;gap:10px}.fd-hold-demo-danger li::before{content:"";flex:none;width:5px;height:5px;margin-top:7px;border-radius:50%;background:var(--d-danger)}
361.fd-hold-demo-action{display:flex;flex-direction:column;align-items:center;gap:10px;flex:none}
362.fd-hold-demo-action small{font:400 12px/1 var(--d-mono);color:var(--d-mut);font-variant-numeric:tabular-nums;min-width:20ch;text-align:center}
363.fd-hold-demo-gone{display:flex;flex-direction:column;align-items:flex-start;gap:4px;flex:none;min-width:200px;font:400 13px/1.4 var(--d-font);color:var(--d-mut)}
364.fd-hold-demo-gone b{font:600 15px/1.2 var(--d-font);color:var(--d-ink)}
365.fd-hold-demo-gone button{margin-top:8px;cursor:pointer;height:32px;padding:0 14px;border-radius:9999px;box-shadow:0 0 0 1px var(--d-line);font:500 13px/1 var(--d-font);color:var(--d-ink)}
366.fd-hold-demo-note{font:400 13px/1.4 var(--d-font);color:#30D158}
367@media (max-width:860px){.fd-hold-demo{grid-template-columns:1fr}.fd-hold-demo-side{display:none}.fd-hold-demo-card{flex-direction:column;align-items:stretch}.fd-hold-demo-action{align-items:stretch}.fd-hold-demo-stats{grid-template-columns:repeat(2,minmax(0,1fr))}.fd-hold-demo-stats div:nth-child(3){border-left:0}.fd-hold-demo-stats div:nth-child(n+3){border-top:1px solid var(--d-line)}.fd-hold-demo .fd-hold,.fd-hold-demo .fd-hold-btn{display:flex;width:100%}}
368`;

More Buttons. Same file contract.

Button Core

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

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.