Glass Menu

A glass button that morphs — one surface, springing open — into its menu and back, with full keyboard support.

  • Liquid Glass
  • Fintech
  • 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-menu.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/liquid-glass/glass-menu.tsx
1/**
2 * GlassMenu — a Liquid Glass button that morphs into its menu and back.
3 *
4 * One glass surface, not two: the capsule's width, height and corner radius
5 * spring from button-size to menu-size (anchored to the button's corner), the
6 * button face cross-fades out and the menu items cascade in. The refraction
7 * map follows the shape every frame. Regular variant — menus carry text.
8 *
9 * Props: label (button text, or aria-label for icon buttons), icon,
10 * items: ({ id, label, icon?, detail?, destructive?, disabled? } | "separator")[],
11 * onSelect(id), align ("start" | "end" — which corner stays anchored),
12 * placement ("down" | "up"), tone ("light" | "dark"), menuWidth (px).
13 * Keyboard: Enter/Space/ArrowDown open (focus first item), ArrowUp opens on
14 * last; Arrow keys/Home/End move; Enter selects; Escape or Tab closes and
15 * returns focus. Click outside closes. role="menu"/"menuitem",
16 * aria-haspopup, aria-expanded, aria-controls.
17 * Reduced motion: shape changes instantly; content fades only.
18 * Browser support: refraction in Chromium; frosted glass elsewhere.
19 */
20import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
21import type { CSSProperties, KeyboardEvent, ReactNode } from "react";
22import { GlassScope, LiquidGlass, useGlassEnv, useSpring } from "./liquid-glass";
23
24export type GlassMenuItem = { id: string; label: string; icon?: ReactNode; detail?: string; destructive?: boolean; disabled?: boolean } | "separator";
25
26export interface GlassMenuProps {
27 label: string;
28 icon?: ReactNode;
29 /** Show only the icon on the button (label becomes aria-label). */
30 iconOnly?: boolean;
31 items: GlassMenuItem[];
32 onSelect?: (id: string) => void;
33 align?: "start" | "end";
34 placement?: "down" | "up";
35 tone?: "light" | "dark";
36 menuWidth?: number;
37 /** Button height in px (default 44). */
38 size?: number;
39 className?: string;
40}
41
42const useIso = typeof window === "undefined" ? useEffect : useLayoutEffect;
43const ITEM_H = 44;
44const SEP_H = 9;
45const PAD = 8;
46const MENU_R = 26;
47
48export function GlassMenu({
49 label,
50 icon,
51 iconOnly = false,
52 items,
53 onSelect,
54 align = "start",
55 placement = "down",
56 tone = "light",
57 menuWidth = 260,
58 size = 44,
59 className,
60}: GlassMenuProps) {
61 const env = useGlassEnv();
62 const uid = useId().replace(/[^a-zA-Z0-9_-]/g, "");
63 const [open, setOpen] = useState(false);
64 const [render, setRender] = useState(false); // keep menu mounted while closing
65 const wrap = useRef<HTMLDivElement | null>(null);
66 const surf = useRef<HTMLElement | null>(null);
67 const btn = useRef<HTMLButtonElement | null>(null);
68 const face = useRef<HTMLSpanElement | null>(null);
69 const menu = useRef<HTMLDivElement | null>(null);
70 const focusOnOpen = useRef<"first" | "last" | null>(null);
71 const closed = useRef({ w: size, h: size });
72
73 const menuH = items.reduce((h, it) => h + (it === "separator" ? SEP_H : ITEM_H), 0) + PAD * 2;
74
75 // progress spring 0 (button) → 1 (menu); geometry is interpolated from it
76 const paint = (p: number) => {
77 const s = surf.current;
78 if (!s) return;
79 const c = closed.current;
80 const q = Math.max(0, p);
81 const w = c.w + (menuWidth - c.w) * q;
82 const h = c.h + (menuH - c.h) * q;
83 const r = Math.min(h / 2, c.h / 2 + (MENU_R - c.h / 2) * Math.min(1, q));
84 s.style.width = `${w.toFixed(2)}px`;
85 s.style.height = `${h.toFixed(2)}px`;
86 s.style.borderRadius = `${r.toFixed(2)}px`;
87 s.style.setProperty("--fd-gmn-p", Math.min(1, Math.max(0, p)).toFixed(3));
88 if (face.current) face.current.style.opacity = String(Math.max(0, 1 - q * 3.2).toFixed(3));
89 if (menu.current) menu.current.style.opacity = String(Math.min(1, Math.max(0, (q - 0.35) / 0.5)).toFixed(3));
90 };
91 const prog = useSpring(0, paint, { stiffness: 340, damping: 27, precision: 0.002 });
92
93 // measure the resting button
94 useIso(() => {
95 const b = btn.current;
96 if (!b) return;
97 closed.current = { w: b.offsetWidth, h: b.offsetHeight };
98 if (!open && prog.value === 0) paint(0);
99 // eslint-disable-next-line react-hooks/exhaustive-deps
100 }, [label, icon, iconOnly, size]);
101
102 const show = useCallback(
103 (where: "first" | "last" | null) => {
104 focusOnOpen.current = where;
105 setRender(true);
106 setOpen(true);
107 },
108 [],
109 );
110 const hide = useCallback((returnFocus = true) => {
111 setOpen(false);
112 if (returnFocus) requestAnimationFrame(() => btn.current?.focus({ preventScroll: true }));
113 }, []);
114
115 useEffect(() => {
116 if (open) {
117 prog.stiffness = 340;
118 prog.damping = 27;
119 if (env.reducedMotion) prog.set(1);
120 else prog.to(1);
121 const where = focusOnOpen.current;
122 if (where) {
123 requestAnimationFrame(() => {
124 const list = menu.current?.querySelectorAll<HTMLElement>("[role=menuitem]:not([aria-disabled=true])");
125 if (!list?.length) return;
126 (where === "first" ? list[0] : list[list.length - 1]).focus({ preventScroll: true });
127 });
128 } else requestAnimationFrame(() => menu.current?.focus({ preventScroll: true }));
129 } else if (render) {
130 prog.stiffness = 420;
131 prog.damping = 32;
132 if (env.reducedMotion) prog.set(0);
133 else prog.to(0);
134 const t = window.setTimeout(() => setRender(false), env.reducedMotion ? 0 : 420);
135 return () => window.clearTimeout(t);
136 }
137 // eslint-disable-next-line react-hooks/exhaustive-deps
138 }, [open]);
139
140 // outside click closes
141 useEffect(() => {
142 if (!open) return;
143 const onDown = (e: PointerEvent) => {
144 if (!wrap.current?.contains(e.target as Node)) hide(false);
145 };
146 document.addEventListener("pointerdown", onDown, true);
147 return () => document.removeEventListener("pointerdown", onDown, true);
148 }, [open, hide]);
149
150 const onBtnKey = (e: KeyboardEvent<HTMLButtonElement>) => {
151 if (e.key === "ArrowDown") {
152 e.preventDefault();
153 show("first");
154 } else if (e.key === "ArrowUp") {
155 e.preventDefault();
156 show("last");
157 }
158 };
159 const onMenuKey = (e: KeyboardEvent<HTMLDivElement>) => {
160 const list = Array.from(menu.current?.querySelectorAll<HTMLElement>("[role=menuitem]:not([aria-disabled=true])") ?? []);
161 const i = list.indexOf(document.activeElement as HTMLElement);
162 if (e.key === "Escape") {
163 e.preventDefault();
164 hide();
165 } else if (e.key === "Tab") {
166 hide(false);
167 } else if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Home" || e.key === "End") {
168 e.preventDefault();
169 const n = e.key === "Home" ? 0 : e.key === "End" ? list.length - 1 : (i + (e.key === "ArrowDown" ? 1 : -1) + list.length) % list.length;
170 list[n]?.focus();
171 }
172 };
173
174 const choose = (id: string) => {
175 onSelect?.(id);
176 hide();
177 };
178
179 return (
180 <GlassScope>
181 <div ref={wrap} className={["fd-gmn", `fd-gmn--${align}`, `fd-gmn--${placement}`, `fd-gmn--${tone}`, open ? "is-open" : "", className ?? ""].filter(Boolean).join(" ")} style={{ "--fd-gmn-h": `${size}px` } as CSSProperties}>
182 <style>{css}</style>
183 <button
184 ref={btn}
185 type="button"
186 className={`fd-gmn-btn${iconOnly ? " is-icon" : ""}`}
187 aria-haspopup="menu"
188 aria-expanded={open}
189 aria-controls={`fd-gmn-${uid}`}
190 aria-label={iconOnly ? label : undefined}
191 onClick={() => (open ? hide() : show(null))}
192 onKeyDown={onBtnKey}
193 >
194 {icon ? <span className="fd-gmn-ico" aria-hidden="true">{icon}</span> : null}
195 {!iconOnly ? <span className="fd-gmn-lbl">{label}</span> : null}
196 {!iconOnly ? (
197 <svg className="fd-gmn-chev" viewBox="0 0 12 12" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
198 <path d="m3 4.5 3 3 3-3" />
199 </svg>
200 ) : null}
201 </button>
202 <LiquidGlass ref={surf} className="fd-gmn-surf" variant="regular" tone={tone} radius="auto" interactive={!open} aria-hidden={!render || undefined}>
203 <span ref={face} className="fd-gmn-face" aria-hidden="true">
204 {icon ? <span className="fd-gmn-ico">{icon}</span> : null}
205 {!iconOnly ? <span className="fd-gmn-lbl">{label}</span> : null}
206 {!iconOnly ? (
207 <svg className="fd-gmn-chev" viewBox="0 0 12 12" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
208 <path d="m3 4.5 3 3 3-3" />
209 </svg>
210 ) : null}
211 </span>
212 {render ? (
213 <div ref={menu} id={`fd-gmn-${uid}`} role="menu" aria-label={label} tabIndex={-1} className="fd-gmn-menu" style={{ width: menuWidth }} onKeyDown={onMenuKey}>
214 {items.map((it, i) =>
215 it === "separator" ? (
216 <div key={`s${i}`} role="separator" className="fd-gmn-sep" />
217 ) : (
218 <div
219 key={it.id}
220 role="menuitem"
221 tabIndex={-1}
222 aria-disabled={it.disabled || undefined}
223 className={`fd-gmn-item${it.destructive ? " is-danger" : ""}`}
224 style={{ "--i": i } as CSSProperties}
225 onClick={() => !it.disabled && choose(it.id)}
226 onKeyDown={(e) => {
227 if ((e.key === "Enter" || e.key === " ") && !it.disabled) {
228 e.preventDefault();
229 choose(it.id);
230 }
231 }}
232 >
233 <span className="fd-gmn-item-ico" aria-hidden="true">{it.icon}</span>
234 <span className="fd-gmn-item-lbl">{it.label}</span>
235 {it.detail ? <span className="fd-gmn-item-detail">{it.detail}</span> : null}
236 </div>
237 ),
238 )}
239 </div>
240 ) : null}
241 </LiquidGlass>
242 </div>
243 </GlassScope>
244 );
245}
246
247const css = `
248.fd-gmn,.fd-gmn *,.fd-gmn *::before,.fd-gmn *::after{box-sizing:border-box}
249.fd-gmn{position:relative;display:inline-block;vertical-align:middle;margin:0;padding:0;color:#0B0B0F;font:600 15px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:-0.01em}
250.fd-gmn--dark{color:#F2F2F7}
251.fd-gmn.is-open{z-index:50}
252.fd-gmn-btn{position:relative;z-index:2;display:inline-flex;align-items:center;justify-content:center;gap:8px;height:var(--fd-gmn-h);min-width:var(--fd-gmn-h);margin:0;padding:0 16px;border:0;border-radius:999px;background:transparent;color:transparent;
253 font:inherit;letter-spacing:inherit;cursor:pointer;-webkit-appearance:none;appearance:none;-webkit-tap-highlight-color:transparent}
254.fd-gmn-btn.is-icon{width:var(--fd-gmn-h);padding:0}
255.fd-gmn-btn > *{visibility:hidden}
256.fd-gmn.is-open .fd-gmn-btn{pointer-events:none}
257.fd-gmn-btn:focus-visible{outline:2px solid color-mix(in srgb,#0A84FF 70%,transparent);outline-offset:2px}
258.fd-gmn-surf{position:absolute !important;top:0;z-index:1;display:block;overflow:hidden;margin:0;padding:0;pointer-events:none;will-change:width,height}
259.fd-gmn.is-open .fd-gmn-surf{pointer-events:auto;z-index:3}
260.fd-gmn--start .fd-gmn-surf{left:0}
261.fd-gmn--end .fd-gmn-surf{right:0}
262.fd-gmn--up .fd-gmn-surf{top:auto;bottom:0}
263.fd-gmn-face{position:absolute;top:0;left:0;display:inline-flex;align-items:center;justify-content:center;gap:8px;height:var(--fd-gmn-h);min-width:var(--fd-gmn-h);margin:0;padding:0 16px;white-space:nowrap;color:inherit;font:inherit}
264.fd-gmn--end .fd-gmn-face{left:auto;right:0}
265.fd-gmn--up .fd-gmn-face{top:auto;bottom:0}
266.fd-gmn-btn.is-icon + .fd-gmn-surf .fd-gmn-face{padding:0;width:var(--fd-gmn-h)}
267.fd-gmn-ico,.fd-gmn-ico svg{display:block;width:20px;height:20px;margin:0;padding:0}
268.fd-gmn-lbl{display:block;margin:0;padding:0}
269.fd-gmn-chev{display:block;opacity:0.6;margin:0 -2px 0 -2px}
270.fd-gmn-menu{position:absolute;top:0;left:0;margin:0;padding:${PAD}px;outline:none;opacity:0}
271.fd-gmn--end .fd-gmn-menu{left:auto;right:0}
272.fd-gmn--up .fd-gmn-menu{top:auto;bottom:0}
273.fd-gmn-item{display:flex;align-items:center;gap:12px;height:${ITEM_H}px;margin:0;padding:0 14px 0 12px;border-radius:${MENU_R - PAD}px;color:inherit;
274 font:500 15px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:-0.01em;text-align:left;cursor:pointer;outline:none;white-space:nowrap;
275 transform:translateY(calc((1 - var(--fd-gmn-p,0)) * -6px));transition:background-color var(--fd-dur-1) linear}
276.fd-gmn--up .fd-gmn-item{transform:translateY(calc((1 - var(--fd-gmn-p,0)) * 6px))}
277.fd-gmn-item:hover,.fd-gmn-item:focus-visible{background:rgba(11,11,15,0.07)}
278.fd-gmn--dark .fd-gmn-item:hover,.fd-gmn--dark .fd-gmn-item:focus-visible{background:rgba(255,255,255,0.1)}
279.fd-gmn-item:focus-visible{box-shadow:inset 0 0 0 2px color-mix(in srgb,#0A84FF 70%,transparent)}
280.fd-gmn-item[aria-disabled="true"]{opacity:0.4;cursor:default;background:none}
281.fd-gmn-item.is-danger{color:#E5372C}
282.fd-gmn--dark .fd-gmn-item.is-danger{color:#FF6961}
283.fd-gmn-item-ico{display:grid;place-items:center;width:22px;height:22px;flex:none;margin:0;padding:0}
284.fd-gmn-item-ico svg{display:block;width:20px;height:20px}
285.fd-gmn-item-lbl{flex:1;display:block;margin:0;padding:0}
286.fd-gmn-item-detail{display:block;margin:0;padding:0;font:500 13px/1 ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;opacity:0.55}
287.fd-gmn-sep{height:1px;margin:4px 12px;padding:0;background:rgba(11,11,15,0.1)}
288.fd-gmn--dark .fd-gmn-sep{background:rgba(255,255,255,0.12)}
289@media (prefers-reduced-motion: reduce){.fd-gmn-item{transform:none}}
290`;
291
292/* ───────────────────────── demo: trading position context menu ───────────────────────── */
293
294const s = (d: ReactNode) => (
295 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
296 {d}
297 </svg>
298);
299const IC = {
300 more: s(<><circle cx="6" cy="12" r="1.4" fill="currentColor" /><circle cx="12" cy="12" r="1.4" fill="currentColor" /><circle cx="18" cy="12" r="1.4" fill="currentColor" /></>),
301 buy: s(<path d="M12 19V5M6 11l6-6 6 6" />),
302 sell: s(<path d="M12 5v14M6 13l6 6 6-6" />),
303 alert: s(<path d="M6 16.5V11a6 6 0 0 1 12 0v5.5l1.5 2h-15ZM10 20.5h4" />),
304 stop: s(<><circle cx="12" cy="12" r="8" /><path d="M8.5 12h7" /></>),
305 share: s(<path d="M12 4v11M8 7.5 12 4l4 3.5M6 12v6.5a1.5 1.5 0 0 0 1.5 1.5h9a1.5 1.5 0 0 0 1.5-1.5V12" />),
306 close: s(<path d="M6 6l12 12M18 6 6 18" />),
307 range: s(<><rect x="3.5" y="5" width="17" height="15" rx="3" /><path d="M3.5 10h17M8 3v4M16 3v4" /></>),
308 check: s(<path d="m5 12.5 4.5 4.5L19 7.5" />),
309};
310
311/** Deterministic price series (SSR-safe). */
312const SERIES = Array.from({ length: 120 }, (_, i) => {
313 const t = i / 119;
314 return Math.round((182 + Math.sin(t * 9) * 4 + Math.sin(t * 23 + 1) * 1.6 + t * 14 + Math.cos(t * 47) * 0.7) * 100) / 100;
315});
316
317export function Demo() {
318 const [toast, setToast] = useState<string | null>(null);
319 const [range, setRange] = useState("1M");
320 useEffect(() => {
321 if (!toast) return;
322 const t = window.setTimeout(() => setToast(null), 2200);
323 return () => window.clearTimeout(t);
324 }, [toast]);
325 const min = Math.min(...SERIES);
326 const max = Math.max(...SERIES);
327 const pts = SERIES.map((v, i) => `${((i / (SERIES.length - 1)) * 1000).toFixed(1)},${(260 - ((v - min) / (max - min)) * 220).toFixed(1)}`).join(" ");
328 const RANGES = ["1D", "1W", "1M", "3M", "1Y", "All"];
329 return (
330 <div className="fd-gmnd">
331 <style>{demoCss}</style>
332 <div className="fd-gmnd-card">
333 <header className="fd-gmnd-head">
334 <div className="fd-gmnd-tick">
335 <span className="fd-gmnd-logo" aria-hidden="true">N</span>
336 <div>
337 <p className="fd-gmnd-sym">NRDX · Nordlys Energy</p>
338 <p className="fd-gmnd-ex">XSTO · Position: 240 shares</p>
339 </div>
340 </div>
341 <div className="fd-gmnd-actions">
342 <GlassMenu
343 label={range === "All" ? "All time" : range}
344 icon={IC.range}
345 align="end"
346 tone="dark"
347 menuWidth={200}
348 onSelect={setRange}
349 items={RANGES.map((r) => ({ id: r, label: r === "All" ? "All time" : r, icon: r === range ? IC.check : undefined }))}
350 />
351 <GlassMenu
352 label="Position actions"
353 iconOnly
354 icon={IC.more}
355 tone="dark"
356 align="end"
357 menuWidth={272}
358 onSelect={(id) => setToast(
359 {
360 buy: "Buy order ticket opened",
361 sell: "Sell order ticket opened",
362 alert: "Alert set at SEK 200.00",
363 stop: "Trailing stop set at −4%",
364 share: "Link to NRDX copied",
365 close: "Close position — confirm in ticket",
366 }[id] ?? id,
367 )}
368 items={[
369 { id: "buy", label: "Buy more", icon: IC.buy, detail: "B" },
370 { id: "sell", label: "Sell", icon: IC.sell, detail: "S" },
371 "separator",
372 { id: "alert", label: "Price alert", icon: IC.alert },
373 { id: "stop", label: "Trailing stop", icon: IC.stop, detail: "−4%" },
374 { id: "share", label: "Share", icon: IC.share },
375 "separator",
376 { id: "close", label: "Close position", icon: IC.close, destructive: true },
377 ]}
378 />
379 </div>
380 </header>
381 <div className="fd-gmnd-price">
382 <span className="fd-gmnd-big">SEK 198.42</span>
383 <span className="fd-gmnd-chg">+12.86 (6.93%)</span>
384 </div>
385 <p className="fd-gmnd-meta">Market value SEK 47 620.80 · Unrealised +SEK 3 086.40</p>
386 <svg className="fd-gmnd-chart" viewBox="0 0 1000 280" preserveAspectRatio="none" aria-label="Price chart, up 6.93% this month" role="img">
387 <defs>
388 <linearGradient id="fdgmndfill" x1="0" y1="0" x2="0" y2="1">
389 <stop offset="0" stopColor="#30D158" stopOpacity="0.35" />
390 <stop offset="1" stopColor="#30D158" stopOpacity="0" />
391 </linearGradient>
392 </defs>
393 {[40, 100, 160, 220].map((y) => (
394 <line key={y} x1="0" x2="1000" y1={y} y2={y} stroke="rgba(255,255,255,0.07)" strokeWidth="1" vectorEffect="non-scaling-stroke" />
395 ))}
396 <polygon points={`0,280 ${pts} 1000,280`} fill="url(#fdgmndfill)" />
397 <polyline points={pts} fill="none" stroke="#30D158" strokeWidth="2.25" vectorEffect="non-scaling-stroke" strokeLinejoin="round" />
398 </svg>
399 <dl className="fd-gmnd-stats">
400 {[
401 ["Open", "196.10"],
402 ["Day range", "195.02 – 199.80"],
403 ["Volume", "3.21 M"],
404 ["P/E", "18.4"],
405 ["Mkt cap", "SEK 61.2 bn"],
406 ["Div. yield", "2.6%"],
407 ].map(([k, v]) => (
408 <div key={k}>
409 <dt>{k}</dt>
410 <dd>{v}</dd>
411 </div>
412 ))}
413 </dl>
414 </div>
415 <div className={`fd-gmnd-toast${toast ? " is-on" : ""}`} role="status" aria-live="polite">
416 {toast}
417 </div>
418 </div>
419 );
420}
421
422const F = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;
423const D = `"SF Pro Display",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Roboto,sans-serif`;
424const M = `ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace`;
425const demoCss = `
426.fd-gmnd,.fd-gmnd *,.fd-gmnd *::before,.fd-gmnd *::after{box-sizing:border-box}
427.fd-gmnd{position:relative;min-height:100vh;width:100%;margin:0;padding:48px 20px;display:flex;align-items:center;justify-content:center;overflow:hidden;
428 background:radial-gradient(700px 400px at 70% 10%,rgba(48,209,88,0.18),transparent 60%),radial-gradient(600px 500px at 10% 90%,rgba(10,132,255,0.16),transparent 60%),#0B0B0F;color:#F2F2F7;font:400 15px/1.4 ${F};letter-spacing:normal}
429.fd-gmnd-card{position:relative;width:100%;max-width:760px;margin:0;padding:24px;border-radius:28px;background:#16161B;box-shadow:inset 0 0 0 0.5px rgba(255,255,255,0.08),0 40px 80px -40px rgba(0,0,0,0.9)}
430.fd-gmnd-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:0 0 20px;padding:0}
431.fd-gmnd-tick{display:flex;align-items:center;gap:12px;min-width:0}
432.fd-gmnd-logo{display:grid;place-items:center;flex:none;width:44px;height:44px;border-radius:12px;background:linear-gradient(135deg,#64D2FF,#5E5CE6);font:800 20px/1 ${D};color:#fff}
433.fd-gmnd-sym{margin:0;font:700 17px/1.2 ${F};letter-spacing:-0.01em;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
434.fd-gmnd-ex{margin:2px 0 0;font:500 13px/1.2 ${F};color:#8E8E99}
435.fd-gmnd-actions{display:flex;gap:10px;align-items:center;flex:none}
436.fd-gmnd-price{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin:0 0 4px}
437.fd-gmnd-big{font:700 clamp(2.25rem,6vw,3.25rem)/1 ${D};letter-spacing:-0.035em;color:#fff;font-variant-numeric:tabular-nums}
438.fd-gmnd-chg{font:600 17px/1 ${F};color:#30D158;font-variant-numeric:tabular-nums}
439.fd-gmnd-meta{margin:0 0 16px;font:500 13px/1.4 ${F};color:#8E8E99}
440.fd-gmnd-chart{display:block;width:100%;height:220px;margin:0 0 16px}
441.fd-gmnd-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:0;margin:0;padding:0;border-top:0.5px solid rgba(255,255,255,0.1)}
442.fd-gmnd-stats div{margin:0;padding:12px 0;border-bottom:0.5px solid rgba(255,255,255,0.08)}
443.fd-gmnd-stats dt{margin:0 0 2px;font:500 12px/1.2 ${F};color:#8E8E99}
444.fd-gmnd-stats dd{margin:0;font:500 15px/1.3 ${M};color:#F2F2F7}
445.fd-gmnd-toast{position:absolute;left:50%;bottom:28px;margin:0;padding:12px 18px;border-radius:999px;background:#F2F2F7;color:#0B0B0F;font:600 14px/1 ${F};white-space:nowrap;
446 transform:translate(-50%,20px);opacity:0;transition:opacity 240ms cubic-bezier(0.22,1,0.36,1),transform 420ms cubic-bezier(0.34,1.56,0.64,1);pointer-events:none}
447.fd-gmnd-toast.is-on{opacity:1;transform:translate(-50%,0)}
448@media (max-width:560px){
449 .fd-gmnd{padding:20px 12px;align-items:flex-start}
450 .fd-gmnd-card{padding:18px}
451 .fd-gmnd-ex{display:none}
452 .fd-gmnd-sym{font-size:15px}
453 .fd-gmnd-tick{gap:10px}
454 .fd-gmnd-logo{width:38px;height:38px;font-size:17px}
455 .fd-gmnd-actions{gap:8px}
456 .fd-gmnd-stats{grid-template-columns:repeat(2,1fr)}
457 .fd-gmnd-chart{height:160px}
458}
459`;

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.