Glass Toolbar

Grouped glass toolbars with fixed spacers, a sliding selection lens, labelled icons and a soft scroll-edge blur.

  • Liquid Glass
  • Developer tools
  • React only · 0 deps
  • 2 files
Full screen

Browser support · Real refraction (SVG displacement inside backdrop-filter) in Chromium browsers. Safari and Firefox fall back to a frosted blur with the same rim and highlights.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/glass-toolbar.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/liquid-glass/glass-toolbar.tsx
1/**
2 * GlassToolbar — grouped Liquid Glass toolbar with a scroll edge effect.
3 *
4 * HIG rules baked in:
5 * - Related actions share one glass capsule (a group); groups are separated by
6 * fixed spacers, never stacked glass on glass.
7 * - Icon groups and text groups are separate (don't mix in one capsule).
8 * - Every icon has an accessibility label (also shown as a tooltip).
9 * - The selected tool sits under a soft lens that slides between items.
10 * - <ScrollEdge> softly blurs + fades content scrolling under a bar.
11 *
12 * Components:
13 * <GlassToolbar label tone tooltips> — role="toolbar"; Arrow keys move between all
14 * items across groups (roving tabindex).
15 * <ToolbarGroup selectable value onChange> — one glass capsule.
16 * <ToolbarItem id label icon? text? onClick?> — icon-only items need `label`.
17 * <ToolbarSpacer flexible?> — fixed gap (default) or flexible.
18 * <ScrollEdge edge="top"|"bottom" size tone> — place inside a scroll
19 * container's positioned parent, above content and below the bar.
20 * Browser support: refraction in Chromium; frosted glass elsewhere.
21 */
22import { Children, createContext, isValidElement, useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
23import type { CSSProperties, KeyboardEvent, ReactNode } from "react";
24import { GlassScope, LiquidGlass, useGlassEnv, useSpring } from "./liquid-glass";
25
26type Tone = "light" | "dark";
27const ToneCtx = createContext<Tone>("light");
28type GroupCtx = { selectable: boolean; value?: string; onChange?: (id: string) => void };
29const GroupContext = createContext<GroupCtx>({ selectable: false });
30
31const useIso = typeof window === "undefined" ? useEffect : useLayoutEffect;
32
33export function GlassToolbar({
34 label,
35 tone = "light",
36 tooltips = "below",
37 children,
38 className,
39 style,
40}: {
41 label: string;
42 tone?: Tone;
43 /** Where icon tooltips appear: "below" for top bars, "above" for bottom bars. */
44 tooltips?: "above" | "below";
45 children: ReactNode;
46 className?: string;
47 style?: CSSProperties;
48}) {
49 const ref = useRef<HTMLDivElement | null>(null);
50 // roving tabindex across every item in every group
51 useEffect(() => {
52 const el = ref.current;
53 if (!el) return;
54 const items = Array.from(el.querySelectorAll<HTMLElement>(".fd-gtb-item:not(:disabled)"));
55 const current = items.find((i) => i.getAttribute("aria-pressed") === "true" || i.getAttribute("aria-checked") === "true") ?? items[0];
56 items.forEach((i) => (i.tabIndex = i === current ? 0 : -1));
57 });
58 const onKey = (e: KeyboardEvent<HTMLDivElement>) => {
59 const items = Array.from(ref.current?.querySelectorAll<HTMLElement>(".fd-gtb-item:not(:disabled)") ?? []);
60 const i = items.indexOf(document.activeElement as HTMLElement);
61 if (i < 0) return;
62 const map: Record<string, number> = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: items.length - 1 };
63 if (!(e.key in map)) return;
64 e.preventDefault();
65 const n = items[(map[e.key] + items.length) % items.length];
66 items.forEach((x) => (x.tabIndex = -1));
67 n.tabIndex = 0;
68 n.focus();
69 };
70 return (
71 <ToneCtx.Provider value={tone}>
72 <GlassScope>
73 <div ref={ref} role="toolbar" aria-label={label} className={["fd-gtb", `fd-gtb--${tone}`, `fd-gtb--tips-${tooltips}`, className ?? ""].filter(Boolean).join(" ")} style={style} onKeyDown={onKey}>
74 <style>{css}</style>
75 {children}
76 </div>
77 </GlassScope>
78 </ToneCtx.Provider>
79 );
80}
81
82export function ToolbarGroup({
83 children,
84 selectable = false,
85 value,
86 onChange,
87 label,
88}: {
89 children: ReactNode;
90 selectable?: boolean;
91 value?: string;
92 onChange?: (id: string) => void;
93 label?: string;
94}) {
95 const tone = useContext(ToneCtx);
96 const env = useGlassEnv();
97 const inner = useRef<HTMLDivElement | null>(null);
98 const lens = useRef<HTMLSpanElement | null>(null);
99 const text = Children.toArray(children).some((c) => isValidElement(c) && (c.props as { text?: string }).text && !(c.props as { icon?: ReactNode }).icon);
100
101 const paint = () => {
102 const l = lens.current;
103 if (!l) return;
104 const st = env.reducedMotion ? 0 : Math.min(0.3, Math.abs(lx.velocity) * 0.0005);
105 l.style.transform = `translateX(${lx.value.toFixed(2)}px) scale(${(1 + st).toFixed(4)}, ${(1 - st * 0.4).toFixed(4)})`;
106 l.style.width = `${lw.value.toFixed(2)}px`;
107 };
108 const lx = useSpring(0, paint, { stiffness: 380, damping: 28 });
109 const lw = useSpring(0, paint, { stiffness: 380, damping: 30 });
110 const first = useRef(true);
111 useIso(() => {
112 if (!selectable) return;
113 const el = inner.current?.querySelector<HTMLElement>(`[data-id="${CSS.escape(value ?? "")}"]`);
114 if (!el || !lens.current) {
115 if (lens.current) lens.current.style.opacity = "0";
116 return;
117 }
118 lens.current.style.opacity = "1";
119 if (first.current || env.reducedMotion) {
120 lx.set(el.offsetLeft);
121 lw.set(el.offsetWidth);
122 first.current = false;
123 } else {
124 lx.to(el.offsetLeft);
125 lw.to(el.offsetWidth);
126 }
127 }, [value, selectable, env.reducedMotion]);
128
129 return (
130 <GroupContext.Provider value={{ selectable, value, onChange }}>
131 <LiquidGlass className={`fd-gtb-group${text ? " is-text" : ""}`} variant="regular" tone={tone} radius="capsule" role="group" aria-label={label}>
132 <div ref={inner} className="fd-gtb-inner">
133 {selectable && <span ref={lens} className="fd-gtb-lens" aria-hidden="true" />}
134 {children}
135 </div>
136 </LiquidGlass>
137 </GroupContext.Provider>
138 );
139}
140
141export function ToolbarItem({
142 id,
143 label,
144 icon,
145 text,
146 onClick,
147 disabled,
148 pressed,
149 shortcut,
150}: {
151 id: string;
152 /** Accessible name + tooltip. Required for icon-only items. */
153 label: string;
154 icon?: ReactNode;
155 /** Text label (use text-only groups). */
156 text?: string;
157 onClick?: () => void;
158 disabled?: boolean;
159 /** Toggle state for non-selectable toggle items. */
160 pressed?: boolean;
161 shortcut?: string;
162}) {
163 const g = useContext(GroupContext);
164 const selected = g.selectable ? g.value === id : pressed;
165 return (
166 <button
167 type="button"
168 className={`fd-gtb-item${text && !icon ? " is-text" : ""}`}
169 data-id={id}
170 aria-label={icon ? label : undefined}
171 aria-pressed={selected === undefined ? undefined : selected}
172 disabled={disabled}
173 onClick={() => {
174 if (g.selectable) g.onChange?.(id);
175 onClick?.();
176 }}
177 >
178 {icon ? <span className="fd-gtb-ico" aria-hidden="true">{icon}</span> : null}
179 {text && !icon ? <span className="fd-gtb-txt">{text}</span> : null}
180 {icon ? (
181 <span className="fd-gtb-tip" role="presentation" aria-hidden="true">
182 {label}
183 {shortcut ? <kbd>{shortcut}</kbd> : null}
184 </span>
185 ) : null}
186 </button>
187 );
188}
189
190export function ToolbarSpacer({ flexible = false }: { flexible?: boolean }) {
191 return <span className={`fd-gtb-spacer${flexible ? " is-flex" : ""}`} aria-hidden="true" />;
192}
193
194/** Scroll edge effect: a progressive blur + fade where content passes under a bar. */
195export function ScrollEdge({ edge = "top", size = 96, tone = "light", style }: { edge?: "top" | "bottom"; size?: number; tone?: Tone; style?: CSSProperties }) {
196 return (
197 <div className={`fd-gtb-edge fd-gtb-edge--${edge} fd-gtb-edge--${tone}`} style={{ height: size, ...style }} aria-hidden="true">
198 <style>{css}</style>
199 <span style={{ "--b": "1px", "--a": "0%", "--z": "45%" } as CSSProperties} />
200 <span style={{ "--b": "3px", "--a": "0%", "--z": "70%" } as CSSProperties} />
201 <span style={{ "--b": "8px", "--a": "0%", "--z": "100%" } as CSSProperties} />
202 </div>
203 );
204}
205
206const css = `
207.fd-gtb,.fd-gtb *,.fd-gtb *::before,.fd-gtb *::after{box-sizing:border-box}
208.fd-gtb{display:flex;align-items:center;gap:0;margin:0;padding:0;background:none;border:0;color:#0B0B0F;font:500 14px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:normal}
209.fd-gtb--dark{color:#F2F2F7}
210.fd-gtb-group{flex:none;height:48px;padding:4px}
211.fd-gtb-inner{position:relative;display:flex;align-items:center;gap:2px;height:100%;margin:0;padding:0}
212.fd-gtb-lens{position:absolute;left:0;top:0;bottom:0;width:0;margin:0;padding:0;border-radius:999px;pointer-events:none;will-change:transform,width;
213 background:rgba(11,11,15,0.08);box-shadow:inset 0 0 0 0.5px rgba(255,255,255,0.5)}
214.fd-gtb--dark .fd-gtb-lens{background:rgba(255,255,255,0.16)}
215.fd-gtb-spacer{display:block;flex:none;width:12px;height:1px;margin:0;padding:0}
216.fd-gtb-spacer.is-flex{flex:1 1 12px}
217.fd-gtb-item{position:relative;display:inline-flex;align-items:center;justify-content:center;flex:none;width:40px;height:40px;margin:0;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;
218 font:600 14px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:-0.005em;text-shadow:none;cursor:pointer;-webkit-appearance:none;appearance:none;-webkit-tap-highlight-color:transparent;
219 transition:background-color var(--fd-dur-1) linear,transform var(--fd-dur-3) var(--fd-spring),color var(--fd-dur-2) var(--fd-ease-out)}
220.fd-gtb-item.is-text{width:auto;padding:0 14px}
221.fd-gtb-item:hover:not(:disabled){background:rgba(11,11,15,0.05)}
222.fd-gtb--dark .fd-gtb-item:hover:not(:disabled){background:rgba(255,255,255,0.08)}
223.fd-gtb-item:active:not(:disabled){transform:scale(0.9);transition-duration:var(--fd-dur-1)}
224.fd-gtb-item[aria-pressed="true"]{color:#007AFF}
225.fd-gtb--dark .fd-gtb-item[aria-pressed="true"]{color:#0A84FF}
226.fd-gtb-item:disabled{opacity:0.35;cursor:default}
227.fd-gtb-item:focus-visible{outline:2px solid color-mix(in srgb,#0A84FF 70%,transparent);outline-offset:2px}
228.fd-gtb-ico,.fd-gtb-ico svg{display:block;width:22px;height:22px;margin:0;padding:0}
229.fd-gtb-txt{display:block;margin:0;padding:0;white-space:nowrap}
230.fd-gtb-tip{position:absolute;left:50%;top:calc(100% + 12px);display:flex;align-items:center;gap:8px;margin:0;padding:7px 10px;border-radius:10px;background:rgba(28,28,34,0.94);color:#F2F2F7;
231 font:600 12px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:0;white-space:nowrap;pointer-events:none;opacity:0;transform:translate(-50%,-4px) scale(0.96);
232 transition:opacity var(--fd-dur-1) linear,transform var(--fd-dur-2) var(--fd-ease-out);z-index:10;box-shadow:0 8px 20px -6px rgba(0,0,0,0.35)}
233.fd-gtb--tips-above .fd-gtb-tip{top:auto;bottom:calc(100% + 12px);transform:translate(-50%,4px) scale(0.96)}
234.fd-gtb-tip kbd{display:inline-block;margin:0;padding:3px 5px;border:0;border-radius:5px;background:rgba(255,255,255,0.14);color:#C7C7CC;font:500 11px/1 ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace}
235.fd-gtb-item:hover .fd-gtb-tip,.fd-gtb-item:focus-visible .fd-gtb-tip{opacity:1;transform:translate(-50%,0) scale(1);transition-delay:350ms,350ms}
236.fd-gtb-item:focus-visible .fd-gtb-tip{transition-delay:0ms,0ms}
237.fd-gtb-edge{position:absolute;left:0;right:0;z-index:2;margin:0;padding:0;pointer-events:none;display:block}
238.fd-gtb-edge--top{top:0}
239.fd-gtb-edge--bottom{bottom:0}
240.fd-gtb-edge span{position:absolute;inset:0;display:block;margin:0;padding:0;-webkit-backdrop-filter:blur(var(--b));backdrop-filter:blur(var(--b))}
241.fd-gtb-edge--top span{-webkit-mask:linear-gradient(180deg,#000 var(--a),transparent var(--z));mask:linear-gradient(180deg,#000 var(--a),transparent var(--z))}
242.fd-gtb-edge--bottom span{-webkit-mask:linear-gradient(0deg,#000 var(--a),transparent var(--z));mask:linear-gradient(0deg,#000 var(--a),transparent var(--z))}
243.fd-gtb-edge--top.fd-gtb-edge--light{background:linear-gradient(180deg,rgba(247,247,249,0.85),rgba(247,247,249,0))}
244.fd-gtb-edge--bottom.fd-gtb-edge--light{background:linear-gradient(0deg,rgba(247,247,249,0.85),rgba(247,247,249,0))}
245.fd-gtb-edge--top.fd-gtb-edge--dark{background:linear-gradient(180deg,rgba(11,11,15,0.85),rgba(11,11,15,0))}
246.fd-gtb-edge--bottom.fd-gtb-edge--dark{background:linear-gradient(0deg,rgba(11,11,15,0.85),rgba(11,11,15,0))}
247@media (prefers-reduced-transparency: reduce){.fd-gtb-edge span{-webkit-backdrop-filter:none;backdrop-filter:none}}
248@media (prefers-reduced-motion: reduce){.fd-gtb-item:active:not(:disabled){transform:none}.fd-gtb-tip{transition-duration:0ms}}
249`;
250
251/* ───────────────────────── demo: a design-doc editor ───────────────────────── */
252
253const s = (d: ReactNode) => (
254 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
255 {d}
256 </svg>
257);
258const IC = {
259 sidebar: s(<><rect x="3.5" y="4.5" width="17" height="15" rx="3" /><path d="M9.5 4.5v15" /></>),
260 undo: s(<path d="M9 7 4.5 11.5 9 16M5 11.5h9.5a5 5 0 0 1 0 10H12" />),
261 redo: s(<path d="m15 7 4.5 4.5L15 16M19 11.5H9.5a5 5 0 0 0 0 10H12" />),
262 select: s(<path d="m5.5 4 13 6.2-5.6 1.8-1.8 5.6Z" />),
263 frame: s(<path d="M8 3.5v17M16 3.5v17M3.5 8h17M3.5 16h17" />),
264 pen: s(<><path d="M12 19.5 19.5 12l-3-3L9 16.5Z" /><path d="m16.5 9-2-6-11 3.5L7 17.5l2-1M4 6.5l5 5" /></>),
265 text: s(<path d="M5.5 6V4.5h13V6M12 4.5v15M9 19.5h6" />),
266 comment: s(<path d="M5 18.5V7a2.5 2.5 0 0 1 2.5-2.5h9A2.5 2.5 0 0 1 19 7v6.5a2.5 2.5 0 0 1-2.5 2.5H9Z" />),
267 bold: s(<path d="M7 4.5h6a3.5 3.5 0 0 1 0 7H7Zm0 7h7a4 4 0 0 1 0 8H7Z" />),
268 italic: s(<path d="M10 4.5h8M6 19.5h8M14 4.5l-4 15" />),
269 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" />),
270 play: s(<path d="M7.5 5.5v13l11-6.5Z" />),
271};
272
273export function Demo() {
274 const [tool, setTool] = useState("select");
275 const [bold, setBold] = useState(false);
276 const [italic, setItalic] = useState(true);
277 const [mode, setMode] = useState("design");
278 return (
279 <div className="fd-gtbd">
280 <style>{demoCss}</style>
281 <div className="fd-gtbd-scroll" tabIndex={0} aria-label="Document canvas">
282 <Canvas />
283 </div>
284 <ScrollEdge edge="top" size={110} />
285 <ScrollEdge edge="bottom" size={110} />
286
287 <div className="fd-gtbd-top">
288 <GlassToolbar label="Document">
289 <ToolbarGroup label="View">
290 <ToolbarItem id="sidebar" label="Toggle sidebar" icon={IC.sidebar} shortcut="⌘ ." />
291 </ToolbarGroup>
292 <ToolbarSpacer />
293 <ToolbarGroup label="History">
294 <ToolbarItem id="undo" label="Undo" icon={IC.undo} shortcut="⌘ Z" />
295 <ToolbarItem id="redo" label="Redo" icon={IC.redo} shortcut="⇧ ⌘ Z" disabled />
296 </ToolbarGroup>
297 <ToolbarSpacer flexible />
298 <ToolbarGroup label="Mode" selectable value={mode} onChange={setMode}>
299 <ToolbarItem id="design" label="Design" text="Design" />
300 <ToolbarItem id="prototype" label="Prototype" text="Prototype" />
301 <ToolbarItem id="dev" label="Dev mode" text="Dev" />
302 </ToolbarGroup>
303 <ToolbarSpacer flexible />
304 <ToolbarGroup label="Share">
305 <ToolbarItem id="play" label="Present" icon={IC.play} shortcut="⌥ ⌘ ↵" />
306 </ToolbarGroup>
307 <ToolbarSpacer />
308 <ToolbarGroup label="Publish">
309 <ToolbarItem id="share" label="Share" text="Share" />
310 </ToolbarGroup>
311 </GlassToolbar>
312 </div>
313
314 <div className="fd-gtbd-bottom">
315 <GlassToolbar label="Tools" tooltips="above">
316 <ToolbarGroup label="Tools" selectable value={tool} onChange={setTool}>
317 <ToolbarItem id="select" label="Move" icon={IC.select} shortcut="V" />
318 <ToolbarItem id="frame" label="Frame" icon={IC.frame} shortcut="F" />
319 <ToolbarItem id="pen" label="Pen" icon={IC.pen} shortcut="P" />
320 <ToolbarItem id="text" label="Text" icon={IC.text} shortcut="T" />
321 <ToolbarItem id="comment" label="Comment" icon={IC.comment} shortcut="C" />
322 </ToolbarGroup>
323 <ToolbarSpacer />
324 <ToolbarGroup label="Format">
325 <ToolbarItem id="bold" label="Bold" icon={IC.bold} pressed={bold} onClick={() => setBold((b) => !b)} shortcut="⌘ B" />
326 <ToolbarItem id="italic" label="Italic" icon={IC.italic} pressed={italic} onClick={() => setItalic((b) => !b)} shortcut="⌘ I" />
327 <ToolbarItem id="link" label="Link" icon={IC.link} shortcut="⌘ K" />
328 </ToolbarGroup>
329 </GlassToolbar>
330 </div>
331 </div>
332 );
333}
334
335function Canvas() {
336 return (
337 <div className="fd-gtbd-doc">
338 <p className="fd-gtbd-crumb">Orbit Pay / Checkout v3 / Spec</p>
339 <h1 className="fd-gtbd-h1">One-tap checkout</h1>
340 <p className="fd-gtbd-lead">
341 Reduce the card flow from four screens to one sheet. Target: +6.5% conversion on mobile web, no increase in chargebacks. Ships behind
342 <code>checkout_v3</code> to 10% of EU traffic on 12 Nov.
343 </p>
344 <div className="fd-gtbd-boards">
345 <div className="fd-gtbd-board">
346 <span className="fd-gtbd-label">01 · Cart</span>
347 <div className="fd-gtbd-phone fd-gtbd-phone--a">
348 <i /> <i /> <i />
349 <b />
350 </div>
351 </div>
352 <div className="fd-gtbd-board">
353 <span className="fd-gtbd-label">02 · Sheet</span>
354 <div className="fd-gtbd-phone fd-gtbd-phone--b">
355 <i /> <i />
356 <b />
357 </div>
358 </div>
359 <div className="fd-gtbd-board">
360 <span className="fd-gtbd-label">03 · Done</span>
361 <div className="fd-gtbd-phone fd-gtbd-phone--c">
362 <em />
363 <b />
364 </div>
365 </div>
366 </div>
367 <h2 className="fd-gtbd-h2">Flow</h2>
368 <ol className="fd-gtbd-ol">
369 <li>Customer taps <strong>Pay €48.20</strong>. The sheet rises with saved card and shipping prefilled.</li>
370 <li>Biometric confirm. On failure, fall back to 3-D Secure inline — never a redirect.</li>
371 <li>Receipt animates in place; the order number is copyable and the sheet dismisses after 2.4 s.</li>
372 </ol>
373 <h2 className="fd-gtbd-h2">Tokens</h2>
374 <table className="fd-gtbd-table">
375 <thead>
376 <tr>
377 <th scope="col">Token</th>
378 <th scope="col">Light</th>
379 <th scope="col">Dark</th>
380 </tr>
381 </thead>
382 <tbody>
383 {[
384 ["accent", "#007AFF", "#0A84FF"],
385 ["success", "#34C759", "#30D158"],
386 ["sheet.radius", "34px", "34px"],
387 ["sheet.inset", "8px", "8px"],
388 ["button.height", "54px", "54px"],
389 ].map(([a, b, c]) => (
390 <tr key={a}>
391 <td><code>{a}</code></td>
392 <td>{b.startsWith("#") ? <span className="fd-gtbd-sw" style={{ background: b }} /> : null}{b}</td>
393 <td>{c.startsWith("#") ? <span className="fd-gtbd-sw" style={{ background: c }} /> : null}{c}</td>
394 </tr>
395 ))}
396 </tbody>
397 </table>
398 <h2 className="fd-gtbd-h2">Open questions</h2>
399 <p className="fd-gtbd-p">
400 Do we keep the promo-code field visible, or tuck it behind “Add code”? Data from the Lisbon pilot says the visible field costs us 1.1 points of
401 conversion but raises AOV by €3.40. Leaning towards hiding it for returning customers only.
402 </p>
403 <p className="fd-gtbd-p">
404 Apple Pay and Google Pay stay as the first option where available; the card sheet is the fallback. Klarna moves to the payment method picker.
405 </p>
406 </div>
407 );
408}
409
410const F = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;
411const D = `"SF Pro Display",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Roboto,sans-serif`;
412const M = `ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace`;
413const demoCss = `
414.fd-gtbd,.fd-gtbd *,.fd-gtbd *::before,.fd-gtbd *::after{box-sizing:border-box}
415.fd-gtbd{position:relative;height:100vh;min-height:640px;width:100%;margin:0;padding:0;overflow:hidden;background:#F7F7F9;color:#0B0B0F;font:400 15px/1.5 ${F};letter-spacing:normal}
416.fd-gtbd-scroll{position:absolute;inset:0;overflow:auto;margin:0;padding:0;outline:none;scrollbar-width:none;
417 background-color:#F7F7F9;background-image:radial-gradient(circle,#D4D4DC 1px,transparent 1.2px);background-size:22px 22px}
418.fd-gtbd-scroll::-webkit-scrollbar{display:none}
419.fd-gtbd-top{position:absolute;left:16px;right:16px;top:14px;z-index:3;margin:0;padding:0}
420.fd-gtbd-bottom{position:absolute;left:0;right:0;bottom:22px;z-index:3;display:flex;justify-content:center;margin:0;padding:0 12px}
421.fd-gtbd-doc{max-width:860px;margin:0 auto;padding:110px 32px 160px}
422.fd-gtbd-doc h1,.fd-gtbd-doc h2,.fd-gtbd-doc p,.fd-gtbd-doc ol,.fd-gtbd-doc li,.fd-gtbd-doc table{margin:0;padding:0;border:0;color:inherit}
423.fd-gtbd-crumb{margin:0 0 10px;font:500 13px/1.3 ${F};color:#6B6B76}
424.fd-gtbd-h1{margin:0 0 14px;font:800 clamp(2.5rem,6vw,4rem)/1 ${D};letter-spacing:-0.04em;color:#0B0B0F}
425.fd-gtbd-lead{margin:0 0 32px;max-width:62ch;font:400 1.25rem/1.5 ${F};letter-spacing:-0.01em;color:#3A3A44}
426.fd-gtbd-doc code{margin:0 2px;padding:2px 6px;border-radius:6px;background:#E7E7EC;font:500 0.85em/1 ${M};color:#C2255C}
427.fd-gtbd-boards{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin:0 0 40px}
428.fd-gtbd-board{display:flex;flex-direction:column;gap:8px;margin:0;padding:0}
429.fd-gtbd-label{font:600 12px/1 ${F};color:#6B6B76}
430.fd-gtbd-phone{position:relative;aspect-ratio:9/17;margin:0;padding:22px 14px;border-radius:26px;background:#fff;box-shadow:0 0 0 1px #E2E2E8,0 20px 40px -24px rgba(20,20,40,0.35);display:flex;flex-direction:column;gap:10px;overflow:hidden}
431.fd-gtbd-phone i{display:block;height:46px;border-radius:12px;background:linear-gradient(90deg,#FFD6A5 0 46px,#F2F2F7 46px)}
432.fd-gtbd-phone i:nth-child(2){background:linear-gradient(90deg,#BDE0FE 0 46px,#F2F2F7 46px)}
433.fd-gtbd-phone i:nth-child(3){background:linear-gradient(90deg,#CAFFBF 0 46px,#F2F2F7 46px)}
434.fd-gtbd-phone b{display:block;margin-top:auto;height:44px;border-radius:999px;background:#0B0B0F}
435.fd-gtbd-phone--b{background:linear-gradient(180deg,#E9EEF8,#C9D4EA)}
436.fd-gtbd-phone--b::before{content:"";position:absolute;left:8px;right:8px;bottom:8px;top:42%;border-radius:22px;background:#fff;box-shadow:0 -8px 30px rgba(20,20,40,0.15)}
437.fd-gtbd-phone--b i,.fd-gtbd-phone--b b{position:relative;z-index:1}
438.fd-gtbd-phone--b i:first-child{margin-top:auto}
439.fd-gtbd-phone--b b{margin-top:0;background:#007AFF}
440.fd-gtbd-phone--c{align-items:center;justify-content:center}
441.fd-gtbd-phone--c em{display:block;width:72px;height:72px;margin:auto 0 0;border-radius:50%;background:#34C759;box-shadow:inset 0 0 0 0 #fff;position:relative}
442.fd-gtbd-phone--c em::after{content:"";position:absolute;left:26px;top:18px;width:16px;height:30px;border:solid #fff;border-width:0 5px 5px 0;transform:rotate(45deg)}
443.fd-gtbd-phone--c b{width:100%}
444.fd-gtbd-doc .fd-gtbd-h2{margin:8px 0 12px;font:700 1.5rem/1.2 ${D};letter-spacing:-0.02em;color:#0B0B0F}
445.fd-gtbd-doc .fd-gtbd-ol{margin:0 0 32px;padding:0 0 0 24px;list-style:decimal outside;font:400 17px/1.6 ${F};color:#1C1C22}
446.fd-gtbd-doc .fd-gtbd-ol li{margin:0 0 8px;padding:0 0 0 4px}
447.fd-gtbd-doc .fd-gtbd-ol li::marker{font-weight:600;color:#6B6B76}
448.fd-gtbd-ol strong{font-weight:650}
449.fd-gtbd-table{width:100%;margin:0 0 32px;border-collapse:collapse;font:400 15px/1.4 ${F};background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 0 0 1px #E2E2E8}
450.fd-gtbd-table th{padding:10px 14px;text-align:left;font:600 12px/1.2 ${F};letter-spacing:0.04em;text-transform:uppercase;color:#6B6B76;background:#FAFAFC;border-bottom:1px solid #E7E7EC}
451.fd-gtbd-table td{padding:10px 14px;border-bottom:1px solid #F0F0F3;color:#1C1C22;font:500 14px/1.3 ${M}}
452.fd-gtbd-table td code{margin:0;color:#0B0B0F;background:none;padding:0}
453.fd-gtbd-sw{display:inline-block;width:12px;height:12px;margin:0 8px -1px 0;border-radius:4px;box-shadow:inset 0 0 0 1px rgba(0,0,0,0.08)}
454.fd-gtbd-p{margin:0 0 16px;max-width:66ch;font:400 17px/1.6 ${F};color:#1C1C22}
455@media (max-width:760px){
456 .fd-gtbd-top .fd-gtb > :nth-child(n+5):nth-child(-n+8){display:none}
457 .fd-gtbd-top .fd-gtb > :nth-child(4){flex:1 1 12px}
458 .fd-gtbd-doc{padding:96px 18px 150px}
459 .fd-gtbd-boards{gap:10px}
460 .fd-gtbd-phone{border-radius:18px;padding:14px 8px;gap:6px}
461 .fd-gtbd-phone i{height:28px;border-radius:8px}
462 .fd-gtbd-phone b{height:28px}
463 .fd-gtbd-phone--c em{width:40px;height:40px}
464 .fd-gtbd-phone--c em::after{left:14px;top:8px;width:10px;height:18px;border-width:0 4px 4px 0}
465 .fd-gtbd-bottom .fd-gtb-item{width:36px}
466}
467`;

More Liquid Glass. Same file contract.

Liquid Glass

The material itself: a lens that refracts what’s behind it, with squircle bevel, specular rim, dispersion and spring physics.

Glass Button

glass, glassProminent, clear and clear-prominent capsules in four sizes, with a springy press and a glow from the touch point.

Glass Tab Bar

Floating iOS 26 tab bar: a liquid lens springs between tabs, search sits apart, and it minimizes as you scroll.