Glass Tab Bar

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

  • Liquid Glass
  • Music & media
  • 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-tab-bar.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/liquid-glass/glass-tab-bar.tsx
1/**
2 * GlassTabBar — iOS 26-style floating tab bar in Liquid Glass.
3 *
4 * - Floats above content as one regular-glass capsule; Search sits apart at
5 * the trailing end in its own circular glass button (HIG).
6 * - The selection is a liquid lens: it springs between tabs, stretching along
7 * its travel and settling with a small overshoot. Drag across the bar to
8 * scrub the lens; it snaps to the nearest tab on release.
9 * - `minimizeOnScroll`: pass the scroll container's ref. Scrolling down
10 * collapses the bar to the active tab + search; scrolling up expands it.
11 *
12 * Props: tabs ({ id, label, icon }), value, onChange, onSearch,
13 * searchLabel, minimizeOnScroll (RefObject<HTMLElement>), tone, accent,
14 * className. Keyboard: Arrow keys / Home / End move between tabs (roving
15 * tabindex), Enter/Space activate. role="tablist" + aria-selected.
16 * Browser support: refraction in Chromium; frosted fallback elsewhere.
17 * Reduced motion: lens jumps without stretch; minimize is a quick fade.
18 */
19import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
20import type { CSSProperties, KeyboardEvent, ReactNode, RefObject } from "react";
21import { GlassScope, LiquidGlass, createVelocityTracker, useGlassEnv, useSpring } from "./liquid-glass";
22
23export type GlassTab = { id: string; label: string; icon: ReactNode };
24
25export interface GlassTabBarProps {
26 tabs: GlassTab[];
27 value: string;
28 onChange: (id: string) => void;
29 onSearch?: () => void;
30 searchLabel?: string;
31 /** Scroll container to watch; bar minimizes on scroll down, expands on scroll up. */
32 minimizeOnScroll?: RefObject<HTMLElement | null>;
33 tone?: "light" | "dark";
34 /** Colour of the selected tab's icon + label. */
35 accent?: string;
36 className?: string;
37 style?: CSSProperties;
38}
39
40const useIso = typeof window === "undefined" ? useEffect : useLayoutEffect;
41
42export function GlassTabBar({
43 tabs,
44 value,
45 onChange,
46 onSearch,
47 searchLabel = "Search",
48 minimizeOnScroll,
49 tone = "light",
50 accent,
51 className,
52 style,
53}: GlassTabBarProps) {
54 const env = useGlassEnv();
55 const list = useRef<HTMLDivElement | null>(null);
56 const lens = useRef<HTMLSpanElement | null>(null);
57 const [minimized, setMinimized] = useState(false);
58 const [dragging, setDragging] = useState(false);
59 const active = Math.max(0, tabs.findIndex((t) => t.id === value));
60
61 /* lens springs: x + width, with stretch from velocity */
62 const paint = () => {
63 const n = lens.current;
64 if (!n) return;
65 const v = env.reducedMotion ? 0 : lx.velocity;
66 const stretch = Math.min(0.35, Math.abs(v) * 0.00045);
67 n.style.transform = `translateX(${lx.value.toFixed(2)}px) scale(${(1 + stretch + lp.value * 0.1).toFixed(4)}, ${(1 - stretch * 0.55 + lp.value * 0.1).toFixed(4)})`;
68 n.style.width = `${lw.value.toFixed(2)}px`;
69 };
70 const lx = useSpring(0, paint, { stiffness: 380, damping: 28 });
71 const lw = useSpring(0, paint, { stiffness: 380, damping: 30 });
72 const lp = useSpring(0, paint, { stiffness: 520, damping: 26 }); // press "lift"
73
74 const measure = useCallback(
75 (i: number) => {
76 const l = list.current;
77 const btn = l?.querySelectorAll<HTMLElement>("[role=tab]")[i];
78 if (!l || !btn) return null;
79 return { x: btn.offsetLeft, w: btn.offsetWidth };
80 },
81 [],
82 );
83
84 const moveTo = useCallback(
85 (i: number, instant = false) => {
86 const m = measure(i);
87 if (!m) return;
88 if (instant || env.reducedMotion) {
89 lx.set(m.x);
90 lw.set(m.w);
91 } else {
92 lx.to(m.x);
93 lw.to(m.w);
94 }
95 },
96 [measure, env.reducedMotion, lx, lw],
97 );
98
99 const first = useRef(true);
100 useIso(() => {
101 moveTo(active, first.current);
102 first.current = false;
103 }, [active, moveTo]);
104
105 // keep the lens glued to its tab while the bar resizes (minimize/expand, viewport)
106 useEffect(() => {
107 const l = list.current;
108 if (!l || typeof ResizeObserver === "undefined") return;
109 const ro = new ResizeObserver(() => {
110 if (!draggingRef.current) moveTo(activeRef.current, true);
111 });
112 ro.observe(l);
113 return () => ro.disconnect();
114 }, [moveTo]);
115 const activeRef = useRef(active);
116 activeRef.current = active;
117 const draggingRef = useRef(false);
118
119 /* scrub: drag across the bar, the lens follows the finger and snaps on release */
120 useEffect(() => {
121 const l = list.current;
122 if (!l) return;
123 const vt = createVelocityTracker();
124 let startX = 0;
125 let startLens = 0;
126 let moved = false;
127 let pid = -1;
128 const nearest = (cx: number) => {
129 const btns = Array.from(l.querySelectorAll<HTMLElement>("[role=tab]"));
130 let best = 0, bd = Infinity;
131 btns.forEach((b, i) => {
132 const d = Math.abs(b.offsetLeft + b.offsetWidth / 2 - cx);
133 if (d < bd) (bd = d), (best = i);
134 });
135 return best;
136 };
137 const down = (e: PointerEvent) => {
138 if (e.button !== 0 || l.classList.contains("is-min")) return;
139 pid = e.pointerId;
140 startX = e.clientX;
141 startLens = lx.value;
142 moved = false;
143 vt.reset();
144 vt.push(e.clientX, 0);
145 // the lens lifts when you touch the selected tab
146 const idx = nearest(e.clientX - l.getBoundingClientRect().left);
147 if (idx === activeRef.current && !env.reducedMotion) lp.to(1);
148 };
149 const move = (e: PointerEvent) => {
150 if (e.pointerId !== pid) return;
151 const dx = e.clientX - startX;
152 if (!moved && Math.abs(dx) < 6) return;
153 if (!moved) {
154 moved = true;
155 draggingRef.current = true;
156 setDragging(true);
157 l.setPointerCapture(pid);
158 if (!env.reducedMotion) lp.to(1);
159 }
160 vt.push(e.clientX, 0);
161 const max = l.clientWidth - lw.value;
162 let x = startLens + dx;
163 if (x < 0) x *= 0.3;
164 if (x > max) x = max + (x - max) * 0.3;
165 lx.stiffness = 900;
166 lx.damping = 50;
167 lx.to(x);
168 const idx = nearest(x + lw.value / 2);
169 const m = measure(idx);
170 if (m) lw.to(m.w);
171 };
172 const up = (e: PointerEvent) => {
173 if (e.pointerId !== pid) return;
174 pid = -1;
175 lp.to(0);
176 if (!moved) return;
177 draggingRef.current = false;
178 setDragging(false);
179 lx.stiffness = 380;
180 lx.damping = 28;
181 const { vx } = vt.get();
182 const idx = nearest(lx.value + lw.value / 2 + vx * 0.06);
183 const t = tabs[idx];
184 if (t && t.id !== value) onChange(t.id);
185 else moveTo(idx);
186 // swallow the click that follows a drag
187 const stop = (ev: Event) => {
188 ev.stopPropagation();
189 ev.preventDefault();
190 };
191 l.addEventListener("click", stop, { capture: true, once: true });
192 setTimeout(() => l.removeEventListener("click", stop, { capture: true }), 0);
193 };
194 l.addEventListener("pointerdown", down);
195 l.addEventListener("pointermove", move);
196 l.addEventListener("pointerup", up);
197 l.addEventListener("pointercancel", up);
198 return () => {
199 l.removeEventListener("pointerdown", down);
200 l.removeEventListener("pointermove", move);
201 l.removeEventListener("pointerup", up);
202 l.removeEventListener("pointercancel", up);
203 };
204 }, [tabs, value, onChange, measure, moveTo, env.reducedMotion, lx, lw, lp]);
205
206 /* minimize on scroll down, expand on scroll up */
207 useEffect(() => {
208 const sc = minimizeOnScroll?.current;
209 if (!sc) return;
210 let last = sc.scrollTop;
211 let acc = 0;
212 let raf = 0;
213 const onScroll = () => {
214 if (raf) return;
215 raf = requestAnimationFrame(() => {
216 raf = 0;
217 const y = sc.scrollTop;
218 const d = y - last;
219 last = y;
220 if (y < 24) {
221 acc = 0;
222 setMinimized(false);
223 return;
224 }
225 acc = Math.sign(d) === Math.sign(acc) ? acc + d : d;
226 if (acc > 28) setMinimized(true);
227 else if (acc < -28) setMinimized(false);
228 });
229 };
230 sc.addEventListener("scroll", onScroll, { passive: true });
231 return () => {
232 sc.removeEventListener("scroll", onScroll);
233 cancelAnimationFrame(raf);
234 };
235 }, [minimizeOnScroll]);
236
237 const onKey = (e: KeyboardEvent<HTMLDivElement>) => {
238 const n = tabs.length;
239 const map: Record<string, number> = { ArrowRight: active + 1, ArrowLeft: active - 1, Home: 0, End: n - 1 };
240 if (!(e.key in map)) return;
241 e.preventDefault();
242 const i = (map[e.key] + n) % n;
243 onChange(tabs[i].id);
244 list.current?.querySelectorAll<HTMLElement>("[role=tab]")[i]?.focus();
245 };
246
247 const cls = ["fd-gtab", minimized ? "is-min" : "", dragging ? "is-drag" : "", `fd-gtab--${tone}`, className ?? ""].filter(Boolean).join(" ");
248
249 return (
250 <GlassScope>
251 <nav className={cls} style={{ ...(accent ? { "--fd-gtab-accent": accent } : null), ...style } as CSSProperties} aria-label="Primary">
252 <style>{css}</style>
253 <LiquidGlass className="fd-gtab-bar" variant="regular" tone={tone} radius="capsule">
254 <div ref={list} className={`fd-gtab-list${minimized ? " is-min" : ""}`} role="tablist" aria-orientation="horizontal" onKeyDown={onKey}>
255 <span ref={lens} className="fd-gtab-lens" aria-hidden="true" />
256 {tabs.map((t, i) => (
257 <button
258 key={t.id}
259 type="button"
260 role="tab"
261 id={`fd-gtab-${t.id}`}
262 aria-selected={i === active}
263 tabIndex={i === active ? 0 : -1}
264 className="fd-gtab-tab"
265 onClick={() => {
266 if (minimized) setMinimized(false);
267 else onChange(t.id);
268 }}
269 >
270 <span className="fd-gtab-ico" aria-hidden="true">
271 {t.icon}
272 </span>
273 <span className="fd-gtab-lbl">{t.label}</span>
274 </button>
275 ))}
276 </div>
277 </LiquidGlass>
278 <LiquidGlass as="button" type="button" className="fd-gtab-search" variant="regular" tone={tone} interactive aria-label={searchLabel} onClick={onSearch}>
279 <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" aria-hidden="true">
280 <circle cx="10.5" cy="10.5" r="6.5" />
281 <path d="m15.5 15.5 5 5" />
282 </svg>
283 </LiquidGlass>
284 </nav>
285 </GlassScope>
286 );
287}
288
289const css = `
290.fd-gtab,.fd-gtab *,.fd-gtab *::before,.fd-gtab *::after{box-sizing:border-box}
291.fd-gtab{--fd-gtab-accent:#007AFF;--fd-gtab-h:62px;--fd-gtab-pad:4px;
292 display:flex;align-items:center;gap:10px;margin:0;padding:0;background:none;border:0;
293 font:500 10px/1.1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:0.01em;color:#0B0B0F}
294.fd-gtab--dark{--fd-gtab-accent:#0A84FF}
295.fd-gtab-bar{flex:1 1 auto;min-width:0;height:var(--fd-gtab-h);padding:var(--fd-gtab-pad);transition:max-width var(--fd-dur-3) var(--fd-ease-out);max-width:600px}
296.fd-gtab.is-min .fd-gtab-bar{flex:0 0 auto;max-width:var(--fd-gtab-h)}
297.fd-gtab-list{position:relative;display:flex;height:100%;margin:0;padding:0;touch-action:pan-y;user-select:none;-webkit-user-select:none}
298.fd-gtab-lens{position:absolute;left:0;top:0;bottom:0;width:0;margin:0;padding:0;border-radius:999px;pointer-events:none;transform-origin:50% 50%;will-change:transform,width;
299 background:rgba(11,11,15,0.075);box-shadow:inset 0 0 0 0.5px rgba(255,255,255,0.4)}
300.fd-gtab--dark .fd-gtab-lens{background:rgba(255,255,255,0.14)}
301.fd-gtab.is-drag .fd-gtab-lens{background:rgba(255,255,255,0.12);box-shadow:inset 0 1px 1px rgba(255,255,255,0.9),inset 0 -1px 1px rgba(255,255,255,0.35),0 6px 18px -4px rgba(0,0,0,0.22),inset 0 0 0 0.5px rgba(255,255,255,0.7);
302 -webkit-backdrop-filter:blur(1px) saturate(1.6) brightness(1.08);backdrop-filter:blur(1px) saturate(1.6) brightness(1.08)}
303.fd-gtab-tab{position:relative;flex:1 1 0;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;height:100%;margin:0;padding:0 6px;border:0;border-radius:999px;background:none;color:inherit;
304 font:inherit;letter-spacing:inherit;text-shadow:none;cursor:pointer;-webkit-appearance:none;appearance:none;-webkit-tap-highlight-color:transparent;
305 transition:opacity var(--fd-dur-2) var(--fd-ease-out),color var(--fd-dur-2) var(--fd-ease-out),transform var(--fd-dur-3) var(--fd-spring)}
306.fd-gtab--dark .fd-gtab-tab{color:#F2F2F7}
307.fd-gtab-tab[aria-selected="true"]{color:var(--fd-gtab-accent)}
308.fd-gtab-tab:active{transform:scale(0.94);transition-duration:var(--fd-dur-1)}
309.fd-gtab-ico,.fd-gtab-ico svg{display:block;width:26px;height:26px;margin:0;padding:0}
310.fd-gtab-lbl{display:block;margin:0;padding:0;font:600 10px/1.1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:0.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
311.fd-gtab-tab:focus-visible,.fd-gtab-search:focus-visible{outline:2px solid color-mix(in srgb,#0A84FF 70%,transparent);outline-offset:2px}
312.fd-gtab.is-min .fd-gtab-tab[aria-selected="false"]{flex:0 0 0;width:0;padding:0;opacity:0;pointer-events:none}
313.fd-gtab.is-min .fd-gtab-lbl{opacity:0;height:0}
314.fd-gtab.is-min .fd-gtab-lens{opacity:0}
315.fd-gtab-search{flex:none;margin-left:auto !important;display:flex;align-items:center;justify-content:center;width:var(--fd-gtab-h);height:var(--fd-gtab-h);margin:0;padding:0;color:inherit}
316.fd-gtab-search .fd-lg-glow{mix-blend-mode:normal}
317.fd-gtab--dark .fd-gtab-search{color:#F2F2F7}
318.fd-gtab-search svg{display:block}
319.fd-gtab.is-min{--fd-gtab-h:52px}
320.fd-gtab-bar,.fd-gtab-search{transition:max-width var(--fd-dur-3) var(--fd-ease-out),height var(--fd-dur-3) var(--fd-ease-out),width var(--fd-dur-3) var(--fd-ease-out)}
321@media (prefers-reduced-motion: reduce){
322 .fd-gtab-bar,.fd-gtab-search,.fd-gtab-tab{transition-duration:0ms}
323 .fd-gtab-tab:active{transform:none}
324}
325@media (prefers-contrast: more){.fd-gtab-lens{background:rgba(0,0,0,0.16);box-shadow:inset 0 0 0 1px rgba(0,0,0,0.6)}.fd-gtab--dark .fd-gtab-lens{background:rgba(255,255,255,0.22);box-shadow:inset 0 0 0 1px #fff}}
326`;
327
328/* ───────────────────────── demo: music streaming app ───────────────────────── */
329
330const ico = (d: ReactNode) => (
331 <svg viewBox="0 0 26 26" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
332 {d}
333 </svg>
334);
335const TABS: GlassTab[] = [
336 { id: "home", label: "Home", icon: ico(<path d="M4.5 11.5 13 4.5l8.5 7V21a.5.5 0 0 1-.5.5h-5v-6h-6v6H5a.5.5 0 0 1-.5-.5Z" />) },
337 {
338 id: "new",
339 label: "New",
340 icon: ico(
341 <>
342 <rect x="3.5" y="3.5" width="8" height="8" rx="2" />
343 <rect x="14.5" y="3.5" width="8" height="8" rx="2" />
344 <rect x="3.5" y="14.5" width="8" height="8" rx="2" />
345 <rect x="14.5" y="14.5" width="8" height="8" rx="2" />
346 </>,
347 ),
348 },
349 {
350 id: "radio",
351 label: "Radio",
352 icon: ico(
353 <>
354 <circle cx="13" cy="13" r="2.2" />
355 <path d="M8.3 17.7a6.6 6.6 0 0 1 0-9.4M17.7 8.3a6.6 6.6 0 0 1 0 9.4M5.2 20.8a11 11 0 0 1 0-15.6M20.8 5.2a11 11 0 0 1 0 15.6" />
356 </>,
357 ),
358 },
359 {
360 id: "library",
361 label: "Library",
362 icon: ico(
363 <>
364 <path d="M5 4.5v17M9.5 4.5v17" />
365 <path d="m14 5.2 3.9-1 4 16.6-3.9 1Z" />
366 </>,
367 ),
368 },
369];
370
371const ALBUMS = [
372 { t: "Night Ferry", a: "Saoirse Vale", c: ["#FF5E3A", "#FF2A68", "#5B1B8C"] },
373 { t: "Paper Planets", a: "The Lindqvist Set", c: ["#00C6FB", "#005BEA", "#0B1240"] },
374 { t: "Salt & Signal", a: "Moana Reyes", c: ["#F7B733", "#FC4A1A", "#3D0C02"] },
375 { t: "Low Orbit", a: "KIMIRA", c: ["#43E97B", "#38F9D7", "#06402B"] },
376 { t: "Tender Static", a: "Hollis Park", c: ["#FA709A", "#FEE140", "#7A1F3D"] },
377 { t: "Glasshouse", a: "Odette & the Owls", c: ["#A18CD1", "#FBC2EB", "#2C1F4A"] },
378 { t: "Copper Sun", a: "Ilyas Brandt", c: ["#F83600", "#F9D423", "#3A0F00"] },
379 { t: "Blue Hour Radio", a: "Nadia Okafor", c: ["#4FACFE", "#00F2FE", "#032A4A"] },
380];
381const TRACKS = [
382 ["Lanterns", "Saoirse Vale", "3:41"],
383 ["Heliograph", "KIMIRA", "4:12"],
384 ["Monday Tides", "Moana Reyes", "2:58"],
385 ["Sodium Lights", "Hollis Park", "3:27"],
386 ["Ferris", "The Lindqvist Set", "3:05"],
387 ["Cartography", "Nadia Okafor", "4:46"],
388 ["Small Hours", "Odette & the Owls", "3:33"],
389 ["Gold Leaf", "Ilyas Brandt", "3:18"],
390];
391
392const art = (c: string[], i: number): CSSProperties => ({
393 background: [
394 `radial-gradient(circle at ${20 + ((i * 23) % 60)}% ${25 + ((i * 41) % 50)}%, ${c[1]} 0 18%, transparent 46%)`,
395 `repeating-linear-gradient(${(i * 37) % 180}deg, rgba(255,255,255,0.0) 0 9px, rgba(255,255,255,0.16) 9px 10px)`,
396 `linear-gradient(${135 + i * 20}deg, ${c[0]}, ${c[2]})`,
397 ].join(","),
398});
399
400function MusicScreen({ scrollRef, compact }: { scrollRef: RefObject<HTMLDivElement | null>; compact?: boolean }) {
401 const [tab, setTab] = useState("home");
402 const title = TABS.find((t) => t.id === tab)!.label;
403 return (
404 <div className={`fd-gtd-app${compact ? " is-compact" : ""}`}>
405 <div className="fd-gtd-scroll" ref={scrollRef} tabIndex={0} aria-label={`${title} — scrollable`}>
406 <header className="fd-gtd-head">
407 <p className="fd-gtd-date">Thursday, 29 October</p>
408 <h2 className="fd-gtd-h">{tab === "home" ? "Listen Now" : title}</h2>
409 </header>
410 <section className="fd-gtd-hero" style={art(ALBUMS[0].c, 3)}>
411 <span className="fd-gtd-chip">New album</span>
412 <p className="fd-gtd-hero-t">Night Ferry</p>
413 <p className="fd-gtd-hero-a">Saoirse Vale · 11 songs · Dolby Atmos</p>
414 </section>
415 <h3 className="fd-gtd-sh">Made for you</h3>
416 <div className="fd-gtd-grid">
417 {ALBUMS.map((a, i) => (
418 <figure key={a.t} className="fd-gtd-alb">
419 <div className="fd-gtd-art" style={art(a.c, i)} />
420 <figcaption>
421 <b>{a.t}</b>
422 <span>{a.a}</span>
423 </figcaption>
424 </figure>
425 ))}
426 </div>
427 <h3 className="fd-gtd-sh">Top songs this week</h3>
428 <ol className="fd-gtd-tracks">
429 {TRACKS.map(([t, a, d], i) => (
430 <li key={t}>
431 <span className="fd-gtd-n">{i + 1}</span>
432 <span className="fd-gtd-mini" style={art(ALBUMS[i % ALBUMS.length].c, i)} />
433 <span className="fd-gtd-tt">
434 <b>{t}</b>
435 <span>{a}</span>
436 </span>
437 <span className="fd-gtd-d">{d}</span>
438 </li>
439 ))}
440 </ol>
441 </div>
442 <div className="fd-gtd-dock">
443 <GlassTabBar tabs={TABS} value={tab} onChange={setTab} minimizeOnScroll={scrollRef} accent="#FA2D48" />
444 </div>
445 </div>
446 );
447}
448
449export function Demo() {
450 const phone = useRef<HTMLDivElement | null>(null);
451 const web = useRef<HTMLDivElement | null>(null);
452 return (
453 <div className="fd-gtd">
454 <style>{demoCss}</style>
455 <div className="fd-gtd-web" aria-label="Web layout">
456 <MusicScreen scrollRef={web} />
457 </div>
458 <div className="fd-gtd-phone" aria-label="Phone layout">
459 <div className="fd-gtd-notch" aria-hidden="true" />
460 <MusicScreen scrollRef={phone} compact />
461 </div>
462 </div>
463 );
464}
465
466const F = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;
467const D = `"SF Pro Display",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Roboto,sans-serif`;
468const demoCss = `
469.fd-gtd,.fd-gtd *,.fd-gtd *::before,.fd-gtd *::after{box-sizing:border-box}
470.fd-gtd{position:relative;display:flex;gap:40px;align-items:center;justify-content:center;height:100vh;min-height:720px;width:100%;margin:0;padding:32px;overflow:hidden;
471 background:radial-gradient(1200px 600px at 20% 0%,#FFE4EC 0%,transparent 60%),radial-gradient(900px 600px at 100% 100%,#E2ECFF 0%,transparent 60%),#F7F7F9;color:#0B0B0F;font:400 15px/1.4 ${F};letter-spacing:normal}
472.fd-gtd-web{position:relative;flex:1 1 auto;max-width:860px;height:min(760px,100%);margin:0;border-radius:28px;overflow:hidden;background:#fff;box-shadow:0 40px 80px -40px rgba(20,20,40,0.35),0 0 0 1px rgba(0,0,0,0.06)}
473.fd-gtd-phone{position:relative;flex:none;width:390px;height:min(800px,100%);margin:0;padding:12px;border-radius:56px;background:#0B0B0F;box-shadow:0 40px 80px -30px rgba(20,20,40,0.5),inset 0 0 0 2px #2A2A31}
474.fd-gtd-phone > .fd-gtd-app{border-radius:44px}
475.fd-gtd-notch{position:absolute;left:50%;top:22px;width:120px;height:34px;margin:0 0 0 -60px;border-radius:20px;background:#000;z-index:5}
476.fd-gtd-app{position:relative;height:100%;width:100%;margin:0;overflow:hidden;background:#fff;color:#0B0B0F}
477.fd-gtd-scroll{position:absolute;inset:0;overflow-y:auto;overflow-x:hidden;margin:0;padding:28px 32px 140px;scrollbar-width:none;outline:none}
478.fd-gtd-scroll::-webkit-scrollbar{display:none}
479.fd-gtd-app.is-compact .fd-gtd-scroll{padding:64px 18px 130px}
480.fd-gtd-head{margin:0 0 18px;padding:0}
481.fd-gtd-date{margin:0 0 2px;font:600 13px/1.2 ${F};letter-spacing:0.02em;text-transform:uppercase;color:#6B6B76}
482.fd-gtd-h{margin:0;font:800 2.125rem/1.1 ${D};letter-spacing:-0.025em;color:#0B0B0F}
483.fd-gtd-hero{position:relative;display:flex;flex-direction:column;justify-content:flex-end;height:260px;margin:0 0 28px;padding:22px;border-radius:22px;color:#fff;overflow:hidden}
484.fd-gtd-app.is-compact .fd-gtd-hero{height:220px}
485.fd-gtd-chip{align-self:flex-start;margin:0 0 auto;padding:6px 10px;border-radius:999px;background:rgba(0,0,0,0.28);font:700 11px/1 ${F};letter-spacing:0.06em;text-transform:uppercase;color:#fff}
486.fd-gtd-hero-t{margin:0 0 4px;font:800 2.75rem/1 ${D};letter-spacing:-0.035em;color:#fff}
487.fd-gtd-hero-a{margin:0;font:500 15px/1.3 ${F};color:rgba(255,255,255,0.88)}
488.fd-gtd-sh{margin:0 0 12px;font:700 1.25rem/1.2 ${D};letter-spacing:-0.01em;color:#0B0B0F}
489.fd-gtd-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin:0 0 28px}
490.fd-gtd-app.is-compact .fd-gtd-grid{grid-template-columns:repeat(2,1fr);gap:14px}
491.fd-gtd-alb{display:block;margin:0;padding:0}
492.fd-gtd-art{display:block;aspect-ratio:1;border-radius:12px;box-shadow:inset 0 0 0 0.5px rgba(0,0,0,0.12)}
493.fd-gtd-alb figcaption{display:block;margin:8px 0 0;padding:0}
494.fd-gtd-alb b{display:block;font:600 13px/1.3 ${F};color:#0B0B0F;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
495.fd-gtd-alb span{display:block;font:400 13px/1.3 ${F};color:#6B6B76}
496.fd-gtd-tracks{list-style:none;margin:0;padding:0}
497.fd-gtd-tracks li{display:flex;align-items:center;gap:12px;margin:0;padding:8px 0;border-bottom:0.5px solid #E7E7EC}
498.fd-gtd-n{width:18px;font:600 15px/1 ${F};color:#6B6B76;text-align:center}
499.fd-gtd-mini{display:block;width:44px;height:44px;flex:none;border-radius:8px}
500.fd-gtd-tt{flex:1;min-width:0;display:flex;flex-direction:column}
501.fd-gtd-tt b{font:600 15px/1.3 ${F};color:#0B0B0F}
502.fd-gtd-tt span{font:400 13px/1.3 ${F};color:#6B6B76}
503.fd-gtd-d{font:500 13px/1 ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;color:#6B6B76}
504.fd-gtd-dock{position:absolute;left:0;right:0;bottom:0;display:flex;justify-content:center;margin:0;padding:0 16px 22px;pointer-events:none;
505 background:linear-gradient(180deg,rgba(255,255,255,0),rgba(255,255,255,0.65) 60%);}
506.fd-gtd-dock > *{pointer-events:auto}
507.fd-gtd-web .fd-gtd-dock .fd-gtab{width:min(520px,100%)}
508.fd-gtd-phone .fd-gtd-dock{padding:0 14px 20px}
509.fd-gtd-phone .fd-gtd-dock .fd-gtab{width:100%}
510.fd-gtd-scroll:focus-visible{outline:2px solid color-mix(in srgb,#007AFF 70%,transparent);outline-offset:-4px}
511@media (max-width:1100px){.fd-gtd-web{display:none}}
512@media (max-width:480px){
513 .fd-gtd{padding:0;min-height:0}
514 .fd-gtd-phone{width:100%;height:100%;padding:0;border-radius:0;box-shadow:none;background:#fff}
515 .fd-gtd-phone > .fd-gtd-app{border-radius:0}
516 .fd-gtd-notch{display:none}
517 .fd-gtd-app.is-compact .fd-gtd-scroll{padding-top:28px}
518}
519`;

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 Toggle

An iOS 26 switch whose knob swells into clear Liquid Glass while pressed or dragged, then settles back to solid.