Icon Button Group

An accessible toolbar of icon toggles and actions with warm tooltips, shortcuts and roving focus.

  • Buttons
  • Productivity / Docs
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers. Tooltip pairing uses :has() with a graceful fallback.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/icon-button-group.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/buttons/icon-button-group.tsx
1/**
2 * IconButtonGroup — a toolbar of icon buttons with tooltips, toggle
3 * (aria-pressed) states, exclusive sub-groups and separators.
4 *
5 * Props: groups: ToolItem[][] — each inner array is a visual group with a
6 * separator between groups. ToolItem = { id, label, icon, shortcut?,
7 * kind?: "toggle" | "action", exclusive?: string, disabled? }.
8 * pressed (Record<id, boolean>) + onToggle(id, next) for toggles,
9 * onAction(id) for actions, size ("sm" | "md"), aria-label, theme.
10 * Items sharing the same `exclusive` key behave like a radio set.
11 *
12 * Browser support: all evergreen browsers.
13 * Accessibility: role="toolbar" with a roving tabindex (ArrowLeft/Right,
14 * Home/End); toggles expose aria-pressed; every button has an aria-label and
15 * aria-keyshortcuts; tooltips (role="tooltip") appear on hover after 400ms
16 * (instantly once warm) and on keyboard focus, and close on Escape.
17 * Reduced motion: tooltip fades only.
18 */
19import { useEffect, useId, useRef, useState } from "react";
20import type { KeyboardEvent, ReactNode } from "react";
21
22export interface ToolItem {
23 id: string;
24 label: string;
25 icon: ReactNode;
26 shortcut?: string;
27 kind?: "toggle" | "action";
28 exclusive?: string;
29 disabled?: boolean;
30}
31
32export interface IconButtonGroupProps {
33 groups: ToolItem[][];
34 pressed?: Record<string, boolean>;
35 onToggle?: (id: string, next: boolean) => void;
36 onAction?: (id: string) => void;
37 size?: "sm" | "md";
38 theme?: "light" | "dark";
39 className?: string;
40 "aria-label"?: string;
41}
42
43export function IconButtonGroup({
44 groups,
45 pressed = {},
46 onToggle,
47 onAction,
48 size = "md",
49 theme,
50 className,
51 "aria-label": ariaLabel = "Formatting",
52}: IconButtonGroupProps) {
53 const uid = useId();
54 const bar = useRef<HTMLDivElement>(null);
55 const flat = groups.flat();
56 const [active, setActive] = useState(flat[0]?.id);
57 const [tip, setTip] = useState<{ id: string; x: number; y: number } | null>(null);
58 const timer = useRef(0);
59 const warm = useRef(0);
60
61 useEffect(() => () => window.clearTimeout(timer.current), []);
62
63 const showTip = (id: string, el: HTMLElement, delay: number) => {
64 window.clearTimeout(timer.current);
65 const go = () => setTip({ id, x: el.offsetLeft + el.offsetWidth / 2, y: el.offsetTop });
66 if (delay <= 0 || Date.now() - warm.current < 600) go();
67 else timer.current = window.setTimeout(go, delay);
68 };
69 const hideTip = () => {
70 window.clearTimeout(timer.current);
71 setTip((t) => {
72 if (t) warm.current = Date.now();
73 return null;
74 });
75 };
76
77 const onKey = (e: KeyboardEvent) => {
78 if (e.key === "Escape") return hideTip();
79 const btns = Array.from(bar.current?.querySelectorAll<HTMLButtonElement>("button:not(:disabled)") ?? []);
80 const i = btns.indexOf(document.activeElement as HTMLButtonElement);
81 let n = -1;
82 if (e.key === "ArrowRight") n = (i + 1) % btns.length;
83 else if (e.key === "ArrowLeft") n = (i - 1 + btns.length) % btns.length;
84 else if (e.key === "Home") n = 0;
85 else if (e.key === "End") n = btns.length - 1;
86 if (n >= 0) {
87 e.preventDefault();
88 btns[n].focus();
89 }
90 };
91
92 const press = (it: ToolItem) => {
93 if (it.kind === "action") return onAction?.(it.id);
94 onToggle?.(it.id, !pressed[it.id]);
95 };
96
97 const tipItem = tip ? flat.find((f) => f.id === tip.id) : null;
98
99 return (
100 <div className={`fd-ibg${className ? " " + className : ""}`} data-size={size} data-fd-theme={theme}>
101 <style>{css}</style>
102 <div ref={bar} className="fd-ibg-bar" role="toolbar" aria-label={ariaLabel} onKeyDown={onKey}>
103 {groups.map((g, gi) => (
104 <div key={gi} className="fd-ibg-group" role="group">
105 {g.map((it) => {
106 const isToggle = it.kind !== "action";
107 return (
108 <button
109 key={it.id}
110 type="button"
111 className="fd-ibg-btn"
112 aria-label={it.label}
113 aria-pressed={isToggle ? !!pressed[it.id] : undefined}
114 aria-keyshortcuts={it.shortcut}
115 aria-describedby={tip?.id === it.id ? `${uid}-tip` : undefined}
116 disabled={it.disabled}
117 tabIndex={active === it.id ? 0 : -1}
118 onClick={() => {
119 press(it);
120 setActive(it.id);
121 }}
122 onFocus={(e) => {
123 setActive(it.id);
124 if (e.currentTarget.matches(":focus-visible")) showTip(it.id, e.currentTarget, 0);
125 }}
126 onBlur={hideTip}
127 onPointerEnter={(e) => e.pointerType === "mouse" && showTip(it.id, e.currentTarget, 400)}
128 onPointerLeave={hideTip}
129 onPointerDown={hideTip}
130 >
131 <span className="fd-ibg-ic">{it.icon}</span>
132 </button>
133 );
134 })}
135 {gi < groups.length - 1 && <span className="fd-ibg-sep" aria-hidden="true" />}
136 </div>
137 ))}
138 {tip && tipItem && (
139 <div id={`${uid}-tip`} role="tooltip" className="fd-ibg-tip" style={{ left: tip.x, top: tip.y }}>
140 {tipItem.label}
141 {tipItem.shortcut && <kbd>{tipItem.shortcut}</kbd>}
142 </div>
143 )}
144 </div>
145 </div>
146 );
147}
148
149const css = `
150:where(.fd-ibg,.fd-ibg *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
151:where(.fd-ibg,.fd-ibg *)::before,:where(.fd-ibg,.fd-ibg *)::after{box-sizing:border-box}
152:where(.fd-ibg svg){display:block;flex-shrink:0}
153:where(.fd-ibg button){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0}
154.fd-ibg{
155 --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;
156 --i-bg:#fff;--i-fg:#0B0B0F;--i-mut:#5F5F6B;--i-hover:rgba(11,11,15,.06);--i-on:rgba(0,122,255,.12);--i-on-fg:#0058C7;--i-line:rgba(11,11,15,.1);--i-ring:rgba(0,122,255,.7);
157 --i-tip-bg:#0B0B0F;--i-tip-fg:#F5F5F7;
158 --i-sh:0 0 0 1px var(--i-line),0 1px 2px rgba(11,11,15,.05),0 8px 24px -12px rgba(11,11,15,.22);
159 display:inline-block;max-width:100%;
160 font:500 12px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;color:var(--i-fg);
161}
162[data-fd-theme="dark"] .fd-ibg,.fd-ibg[data-fd-theme="dark"]{--i-bg:#1C1C22;--i-fg:#F5F5F7;--i-mut:#A1A1AD;--i-hover:rgba(255,255,255,.08);--i-on:rgba(10,132,255,.24);--i-on-fg:#6CB4FF;--i-line:rgba(255,255,255,.1);--i-ring:rgba(10,132,255,.85);--i-tip-bg:#F5F5F7;--i-tip-fg:#0B0B0F;
163 --i-sh:0 0 0 1px var(--i-line),0 1px 0 rgba(255,255,255,.05) inset,0 8px 24px -12px rgba(0,0,0,.7)}
164.fd-ibg[data-size="sm"]{--i-b:28px;--i-ic:16px}
165.fd-ibg[data-size="md"]{--i-b:34px;--i-ic:18px}
166.fd-ibg-bar{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;flex-wrap:wrap;padding:4px;border-radius:calc(var(--i-b)/3.4 + 8px);background:var(--i-bg);box-shadow:var(--i-sh)}
167.fd-ibg-group{display:inline-flex;align-items:center;gap:2px}
168.fd-ibg-sep{width:1px;height:18px;margin:0 4px 0 6px;background:var(--i-line)}
169.fd-ibg-btn{position:relative;display:grid;place-items:center;width:var(--i-b);height:var(--i-b);border-radius:calc(var(--i-b)/3.4 + 4px);
170 appearance:none;-webkit-appearance:none;border:0;outline:0;cursor:pointer;-webkit-tap-highlight-color:transparent;background:transparent;color:var(--i-mut);
171 transform:scale(1);transition:transform 420ms var(--fd-spring),background-color var(--fd-dur-1) var(--fd-ease-out),color var(--fd-dur-1) var(--fd-ease-out)}
172.fd-ibg-ic{display:grid;place-items:center;width:var(--i-ic);height:var(--i-ic)}.fd-ibg-ic svg{width:100%;height:100%}
173@media (hover:hover){.fd-ibg-btn:hover:not(:disabled){background:var(--i-hover);color:var(--i-fg)}}
174.fd-ibg-btn[aria-pressed="true"]{background:var(--i-on);color:var(--i-on-fg)}
175.fd-ibg-btn:active:not(:disabled){transform:scale(.92);transition:transform var(--fd-dur-1) var(--fd-ease-out)}
176.fd-ibg-btn:focus-visible{outline:2px solid var(--i-ring);outline-offset:1px}
177.fd-ibg-btn:disabled{opacity:.35;cursor:not-allowed}
178.fd-ibg-tip{position:absolute;z-index:10;transform:translate(-50%,calc(-100% - 10px));display:flex;align-items:center;gap:8px;padding:6px 8px 6px 10px;border-radius:8px;background:var(--i-tip-bg);color:var(--i-tip-fg);white-space:nowrap;pointer-events:none;
179 font:500 12px/1 inherit;font-family:inherit;box-shadow:0 4px 12px rgba(0,0,0,.25);animation:fd-ibg-tip var(--fd-dur-2) var(--fd-ease-out)}
180.fd-ibg-tip:not(:has(kbd)){padding-right:10px}
181.fd-ibg-tip kbd{font:500 11px/1 ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;padding:3px 5px;border-radius:5px;background:rgba(127,127,140,.28);color:inherit;opacity:.85}
182.fd-ibg-tip::after{content:"";position:absolute;left:50%;top:100%;width:8px;height:8px;margin:-4px 0 0 -4px;background:var(--i-tip-bg);transform:rotate(45deg);border-radius:0 0 2px 0}
183@keyframes fd-ibg-tip{from{opacity:0;transform:translate(-50%,calc(-100% - 6px)) scale(.96)}}
184@media (prefers-reduced-motion:reduce){.fd-ibg-btn,.fd-ibg-btn:active:not(:disabled){transition:background-color 1ms;transform:none}.fd-ibg-tip{animation:fd-ibg-fade 100ms linear}}
185@keyframes fd-ibg-fade{from{opacity:0}}
186@media (prefers-contrast:more){.fd-ibg-btn[aria-pressed="true"]{box-shadow:0 0 0 1px currentColor}}
187`;
188
189/* ───────────────────────────── demo ───────────────────────────── */
190
191const S = ({ children }: { children: ReactNode }) => (
192 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">{children}</svg>
193);
194const icons = {
195 bold: <S><path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z" /></S>,
196 italic: <S><path d="M19 4h-9M14 20H5M15 4L9 20" /></S>,
197 underline: <S><path d="M7 4v7a5 5 0 0 0 10 0V4M5 20h14" /></S>,
198 strike: <S><path d="M4 12h16M16 6.5C15 5.5 13.7 5 12 5c-2.5 0-4 1.2-4 3 0 1.2.8 2 2 2.5M8 17.5c1 1 2.3 1.5 4 1.5 2.5 0 4-1.2 4-3 0-.5-.1-1-.4-1.4" /></S>,
199 code: <S><path d="M8 7l-5 5 5 5M16 7l5 5-5 5M14 4l-4 16" /></S>,
200 link: <S><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1" /></S>,
201 bullets: <S><path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01" /></S>,
202 numbers: <S><path d="M10 6h10M10 12h10M10 18h10M4 5l1.5-1v4M3.8 13.5c.4-1 2.2-1 2.2.2 0 .8-2.2 1.6-2.2 2.8H6.2M4 19.5h1.7l-1 1.2c1-.2 1.6.3 1.6 1s-.8 1.1-2.3.8" /></S>,
203 quote: <S><path d="M5 17c0-4 1-7 4-9M13 17c0-4 1-7 4-9M5 17h4v-4H5zM13 17h4v-4h-4z" /></S>,
204 left: <S><path d="M4 6h16M4 10h10M4 14h16M4 18h10" /></S>,
205 center: <S><path d="M4 6h16M7 10h10M4 14h16M7 18h10" /></S>,
206 right: <S><path d="M4 6h16M10 10h10M4 14h16M10 18h10" /></S>,
207 undo: <S><path d="M9 14L4 9l5-5M4 9h10a6 6 0 0 1 0 12h-3" /></S>,
208 redo: <S><path d="M15 14l5-5-5-5M20 9H10a6 6 0 0 0 0 12h3" /></S>,
209 clear: <S><path d="M5 5l14 14M9 4h10M14 4l-2 6M10 20h5l1-4" /></S>,
210};
211
212const isMac = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
213
214export function Demo() {
215 const [theme, setTheme] = useState<"light" | "dark">("light");
216 const [pressed, setPressed] = useState<Record<string, boolean>>({ "align-left": true, bold: true });
217 const [hist, setHist] = useState<Record<string, boolean>[]>([]);
218 const [future, setFuture] = useState<Record<string, boolean>[]>([]);
219 const [mod, setMod] = useState("Ctrl ");
220 useEffect(() => setMod(isMac() ? "⌘ " : "Ctrl "), []);
221
222 const groups: ToolItem[][] = [
223 [
224 { id: "bold", label: "Bold", icon: icons.bold, shortcut: `${mod}B` },
225 { id: "italic", label: "Italic", icon: icons.italic, shortcut: `${mod}I` },
226 { id: "underline", label: "Underline", icon: icons.underline, shortcut: `${mod}U` },
227 { id: "strike", label: "Strikethrough", icon: icons.strike, shortcut: `${mod}⇧ X` },
228 { id: "code", label: "Inline code", icon: icons.code, shortcut: `${mod}E` },
229 ],
230 [
231 { id: "bullets", label: "Bulleted list", icon: icons.bullets, exclusive: "list" },
232 { id: "numbers", label: "Numbered list", icon: icons.numbers, exclusive: "list" },
233 { id: "quote", label: "Block quote", icon: icons.quote },
234 { id: "link", label: "Link", icon: icons.link, shortcut: `${mod}K` },
235 ],
236 [
237 { id: "align-left", label: "Align left", icon: icons.left, exclusive: "align" },
238 { id: "align-center", label: "Align center", icon: icons.center, exclusive: "align" },
239 { id: "align-right", label: "Align right", icon: icons.right, exclusive: "align" },
240 ],
241 [
242 { id: "undo", label: "Undo", icon: icons.undo, kind: "action", shortcut: `${mod}Z`, disabled: hist.length === 0 },
243 { id: "redo", label: "Redo", icon: icons.redo, kind: "action", shortcut: `${mod}⇧ Z`, disabled: future.length === 0 },
244 { id: "clear", label: "Clear formatting", icon: icons.clear, kind: "action" },
245 ],
246 ];
247 const flat = groups.flat();
248
249 const commit = (next: Record<string, boolean>) => {
250 setHist((h) => [...h, pressed].slice(-20));
251 setFuture([]);
252 setPressed(next);
253 };
254 const toggle = (id: string, on: boolean) => {
255 const ex = flat.find((f) => f.id === id)?.exclusive;
256 const next = { ...pressed };
257 if (ex) flat.filter((f) => f.exclusive === ex).forEach((f) => (next[f.id] = false));
258 if (ex === "align") next[id] = true; // alignment always has one active
259 else next[id] = on;
260 commit(next);
261 };
262 const act = (id: string) => {
263 if (id === "undo" && hist.length) {
264 setFuture((f) => [pressed, ...f]);
265 setPressed(hist[hist.length - 1]);
266 setHist((h) => h.slice(0, -1));
267 } else if (id === "redo" && future.length) {
268 setHist((h) => [...h, pressed]);
269 setPressed(future[0]);
270 setFuture((f) => f.slice(1));
271 } else if (id === "clear") commit({ "align-left": true });
272 };
273 const onKeyDown = (e: React.KeyboardEvent) => {
274 if (!(e.metaKey || e.ctrlKey)) return;
275 const map: Record<string, string> = { b: "bold", i: "italic", u: "underline", e: "code", k: "link" };
276 const id = map[e.key.toLowerCase()];
277 if (id) {
278 e.preventDefault();
279 toggle(id, !pressed[id]);
280 }
281 };
282
283 const align = pressed["align-center"] ? "center" : pressed["align-right"] ? "right" : "left";
284 const marks = [pressed.bold && "b", pressed.italic && "i", pressed.underline && "u", pressed.strike && "s", pressed.code && "c", pressed.link && "l"].filter(Boolean).join(" ");
285 const items = ["Net revenue retention reached 118%", "Median onboarding fell to 9 days", "Two enterprise renewals slipped to Q4"];
286 const List = pressed.numbers ? "ol" : pressed.bullets ? "ul" : "div";
287
288 return (
289 <div className="fd-ibg-demo" data-fd-theme={theme}>
290 <style>{demoCss}</style>
291 <header className="fd-ibg-demo-bar">
292 <span className="fd-ibg-demo-brand">Quill<b>Docs</b></span>
293 <span className="fd-ibg-demo-doc">Board updates / <u>Q3 2026</u></span>
294 <button type="button" className="fd-ibg-demo-theme" aria-label={theme === "dark" ? "Switch to light theme" : "Switch to dark theme"} onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>{theme === "dark" ? "Light" : "Dark"}</button>
295 </header>
296
297 <main className="fd-ibg-demo-main">
298 <div className="fd-ibg-demo-toolbar">
299 <IconButtonGroup groups={groups} pressed={pressed} onToggle={toggle} onAction={act} aria-label="Text formatting" />
300 </div>
301 <article className="fd-ibg-demo-page" onKeyDown={onKeyDown} tabIndex={0} aria-label="Document preview. Use Ctrl or Command with B, I, U to format.">
302 <p className="fd-ibg-demo-kicker">Board update · Quarterly</p>
303 <h1>Q3 2026: efficient growth, slower enterprise close</h1>
304 <p className="fd-ibg-demo-meta">Priya Natarajan, CFO · Updated 27 Oct</p>
305 <div className="fd-ibg-demo-body" style={{ textAlign: align }} data-quote={pressed.quote || undefined}>
306 <p>
307 ARR closed the quarter at $18.4M, and{" "}
308 <span className="fd-ibg-demo-mark" data-marks={marks}>net new ARR grew 22% quarter over quarter</span>
309 , driven by mid-market expansion in logistics and healthcare.
310 </p>
311 <List className="fd-ibg-demo-list" data-kind={pressed.numbers ? "ol" : pressed.bullets ? "ul" : "plain"}>
312 {items.map((t) => (
313 <li key={t}>{t}</li>
314 ))}
315 </List>
316 </div>
317 </article>
318 <p className="fd-ibg-demo-hint">Hover or focus a tool for its tooltip. Arrow keys move through the toolbar.</p>
319 </main>
320 </div>
321 );
322}
323
324const demoCss = `
325:where(.fd-ibg-demo,.fd-ibg-demo *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
326:where(.fd-ibg-demo,.fd-ibg-demo *)::before,:where(.fd-ibg-demo,.fd-ibg-demo *)::after{box-sizing:border-box}
327:where(.fd-ibg-demo button){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0}
328.fd-ibg-demo{--d-font:ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;--d-disp:ui-sans-serif,-apple-system,"SF Pro Display","Inter",sans-serif;--d-mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;
329 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-dur-2:240ms;
330 --d-bg:#F1F1F4;--d-page:#fff;--d-ink:#0B0B0F;--d-mut:#6B6B76;--d-line:rgba(11,11,15,.08);--d-accent:#007AFF;
331 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)}
332.fd-ibg-demo[data-fd-theme="dark"]{--d-bg:#09090B;--d-page:#131317;--d-ink:#F5F5F7;--d-mut:#9A9AA6;--d-line:rgba(255,255,255,.08);--d-accent:#0A84FF}
333.fd-ibg-demo-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;height:56px;padding:0 clamp(16px,4vw,32px);background:var(--d-page);border-bottom:1px solid var(--d-line);font:400 14px/1 var(--d-font);color:var(--d-mut)}
334.fd-ibg-demo-brand{font:500 16px/1 var(--d-disp);letter-spacing:-.02em;color:var(--d-ink)}.fd-ibg-demo-brand b{font-weight:700;color:var(--d-accent)}
335.fd-ibg-demo-doc u{text-decoration:none;color:var(--d-ink);font-weight:500}
336.fd-ibg-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)}
337.fd-ibg-demo-theme:hover{color:var(--d-ink)}.fd-ibg-demo-theme:focus-visible,.fd-ibg-demo-page:focus-visible{outline:2px solid rgba(10,132,255,.7);outline-offset:2px}
338.fd-ibg-demo-main{max-width:760px;margin:0 auto;padding:40px clamp(16px,4vw,32px) 64px;display:flex;flex-direction:column;align-items:center;gap:20px}
339.fd-ibg-demo-toolbar{padding-top:28px;max-width:100%}
340.fd-ibg-demo-page{width:100%;padding:clamp(24px,5vw,56px);border-radius:20px;background:var(--d-page);box-shadow:0 0 0 1px var(--d-line),0 1px 2px rgba(0,0,0,.04),0 24px 48px -28px rgba(0,0,0,.25);outline:0}
341.fd-ibg-demo-kicker{font:500 12px/1 var(--d-font);text-transform:uppercase;letter-spacing:.06em;color:var(--d-accent)}
342.fd-ibg-demo-page h1{margin-top:16px;font:600 clamp(24px,4vw,32px)/1.15 var(--d-disp);letter-spacing:-.025em}
343.fd-ibg-demo-meta{margin-top:12px;font:400 13px/1.4 var(--d-font);color:var(--d-mut)}
344.fd-ibg-demo-body{margin-top:28px;font:400 17px/1.65 var(--d-font);letter-spacing:-.01em}
345.fd-ibg-demo-body[data-quote]>p{padding-left:18px;border-left:3px solid var(--d-accent);color:var(--d-mut)}
346.fd-ibg-demo-list{margin-top:20px;display:flex;flex-direction:column;gap:6px;padding-left:0}
347.fd-ibg-demo-list[data-kind="ul"]{list-style:disc;display:block;padding-left:1.4em}
348.fd-ibg-demo-list[data-kind="ol"]{list-style:decimal;display:block;padding-left:1.6em}
349.fd-ibg-demo-list li{display:list-item;padding:2px 0}.fd-ibg-demo-list[data-kind="plain"] li{display:block}
350.fd-ibg-demo-list li::marker{color:var(--d-accent);font-weight:600}
351.fd-ibg-demo-mark{padding:1px 0;transition:background-color var(--fd-dur-2) var(--fd-ease-out),color var(--fd-dur-2) var(--fd-ease-out);background:rgba(10,132,255,.14);border-radius:4px;box-decoration-break:clone;-webkit-box-decoration-break:clone}
352.fd-ibg-demo-mark[data-marks~="b"]{font-weight:700}.fd-ibg-demo-mark[data-marks~="i"]{font-style:italic}
353.fd-ibg-demo-mark[data-marks~="u"]{text-decoration:underline;text-underline-offset:3px}
354.fd-ibg-demo-mark[data-marks~="s"]{text-decoration:line-through}.fd-ibg-demo-mark[data-marks~="u"][data-marks~="s"]{text-decoration:underline line-through}
355.fd-ibg-demo-mark[data-marks~="c"]{font-family:var(--d-mono);font-size:.9em;padding:2px 6px;background:var(--d-line)}
356.fd-ibg-demo-mark[data-marks~="l"]{color:var(--d-accent);text-decoration:underline;text-underline-offset:3px}
357.fd-ibg-demo-hint{font:400 13px/1.4 var(--d-font);color:var(--d-mut)}
358@media (max-width:640px){.fd-ibg-demo-doc{display:none}}
359`;

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.