Liquid Glass
The material itself: a lens that refracts what’s behind it, with squircle bevel, specular rim, dispersion and spring physics.
Clear-glass playback controls over video: transport, scrubber and volume capsules, with the HIG’s 35% dimming layer.
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.
npx shadcn@latest add https://freedesignmd.com/r/glass-media-controls.json1/**2 * GlassMediaControls — clear Liquid Glass playback controls for video/music.3 *4 * Clear glass is the HIG variant for controls over rich media: highly5 * translucent so the picture shows through, with a refracting rim. Over bright6 * footage, turn on `dim` for the 35% dimming layer so white glyphs stay legible.7 *8 * Pieces (all clear glass, separate capsules — no glass on glass):9 * - transport: back 10 s · play/pause (XL) · forward 10 s10 * - scrubber capsule: elapsed · track · remaining (thumb turns to glass while dragged)11 * - volume capsule: mute toggle + level track12 *13 * Props: playing + onPlayingChange, time + duration (s) + onSeek(s),14 * volume (0–1) + onVolumeChange, muted + onMutedChange, dim, title.15 * Keyboard: all real <button>s and <input type="range">s (Arrow/Home/End).16 * Browser support: refraction in Chromium; frosted glass elsewhere.17 * Reduced motion: no press bounce or knob swell.18 */19import { useEffect, useRef, useState } from "react";20import type { CSSProperties, ReactNode } from "react";21import { GlassScope, LiquidGlass, useGlassEnv, useSpring } from "./liquid-glass";22 23export interface GlassMediaControlsProps {24 playing: boolean;25 onPlayingChange: (p: boolean) => void;26 time: number;27 duration: number;28 onSeek: (t: number) => void;29 volume: number;30 onVolumeChange: (v: number) => void;31 muted: boolean;32 onMutedChange: (m: boolean) => void;33 dim?: boolean;34 /** Accessible name of the media, e.g. "Tidelines — episode 3". */35 title: string;36 className?: string;37}38 39const fmt = (s: number) => {40 const t = Math.max(0, Math.round(s));41 const m = Math.floor(t / 60);42 return `${m}:${String(t % 60).padStart(2, "0")}`;43};44 45/** Thin range track with a knob that becomes glass while dragged. */46function Track({ value, max, step, label, valueText, onChange }: { value: number; max: number; step: number; label: string; valueText: string; onChange: (v: number) => void }) {47 const env = useGlassEnv();48 const knob = useRef<HTMLElement | null>(null);49 const [active, setActive] = useState(false);50 const lift = useSpring(0, (v) => {51 const k = knob.current;52 if (!k) return;53 k.style.setProperty("--fd-gmc-k", v.toFixed(3));54 }, { stiffness: 520, damping: 28 });55 useEffect(() => {56 if (env.reducedMotion) lift.set(active ? 1 : 0);57 else lift.to(active ? 1 : 0);58 }, [active, env.reducedMotion, lift]);59 const pct = max ? Math.min(1, Math.max(0, value / max)) : 0;60 return (61 <span className={`fd-gmc-track${active ? " is-active" : ""}`} style={{ "--p": pct } as CSSProperties}>62 <span className="fd-gmc-rail" aria-hidden="true">63 <span className="fd-gmc-fill" />64 </span>65 <LiquidGlass ref={knob} as="span" className="fd-gmc-knob" variant="clear" radius="capsule" refraction={1.3} shadow={false} aria-hidden="true">66 <span className="fd-gmc-knob-solid" />67 </LiquidGlass>68 <input69 type="range"70 className="fd-gmc-input"71 min={0}72 max={max}73 step={step}74 value={value}75 aria-label={label}76 aria-valuetext={valueText}77 onChange={(e) => onChange(Number(e.target.value))}78 onPointerDown={() => setActive(true)}79 onPointerUp={() => setActive(false)}80 onPointerCancel={() => setActive(false)}81 onBlur={() => setActive(false)}82 />83 </span>84 );85}86 87const I = ({ children, size = 24 }: { children: ReactNode; size?: number }) => (88 <svg viewBox="0 0 24 24" width={size} height={size} fill="currentColor" aria-hidden="true">89 {children}90 </svg>91);92 93export function GlassMediaControls({ playing, onPlayingChange, time, duration, onSeek, volume, onVolumeChange, muted, onMutedChange, dim = false, title, className }: GlassMediaControlsProps) {94 const vol = muted ? 0 : volume;95 return (96 <GlassScope>97 <div className={["fd-gmc", className ?? ""].filter(Boolean).join(" ")} role="group" aria-label={`Playback controls — ${title}`}>98 <style>{css}</style>99 <div className="fd-gmc-transport">100 <LiquidGlass as="button" type="button" className="fd-gmc-btn" variant="clear" dim={dim} interactive aria-label="Back 10 seconds" onClick={() => onSeek(Math.max(0, time - 10))}>101 <Skip back />102 </LiquidGlass>103 <LiquidGlass as="button" type="button" className="fd-gmc-btn fd-gmc-play" variant="clear" dim={dim} interactive aria-label={playing ? "Pause" : "Play"} aria-pressed={playing} onClick={() => onPlayingChange(!playing)}>104 <span className={`fd-gmc-pp${playing ? " is-playing" : ""}`} aria-hidden="true">105 <I size={34}>106 <path className="fd-gmc-pp-a" d="M7 4.6v14.8c0 .9 1 1.4 1.7.9l11.2-7.4a1.1 1.1 0 0 0 0-1.8L8.7 3.7C8 3.2 7 3.7 7 4.6Z" />107 </I>108 <I size={34}>109 <rect x="5.5" y="4" width="4.6" height="16" rx="1.4" />110 <rect x="13.9" y="4" width="4.6" height="16" rx="1.4" />111 </I>112 </span>113 </LiquidGlass>114 <LiquidGlass as="button" type="button" className="fd-gmc-btn" variant="clear" dim={dim} interactive aria-label="Forward 10 seconds" onClick={() => onSeek(Math.min(duration, time + 10))}>115 <Skip />116 </LiquidGlass>117 </div>118 119 <div className="fd-gmc-bar">120 <LiquidGlass className="fd-gmc-scrub" variant="clear" dim={dim}>121 <span className="fd-gmc-time">{fmt(time)}</span>122 <Track value={time} max={duration} step={1} label="Seek" valueText={`${fmt(time)} of ${fmt(duration)}`} onChange={onSeek} />123 <span className="fd-gmc-time">−{fmt(duration - time)}</span>124 </LiquidGlass>125 <LiquidGlass className="fd-gmc-vol" variant="clear" dim={dim}>126 <button type="button" className="fd-gmc-mute" aria-label={muted ? "Unmute" : "Mute"} aria-pressed={muted} onClick={() => onMutedChange(!muted)}>127 <I size={20}>128 <path d="M4 9.2h3.3L12 5v14l-4.7-4.2H4a1 1 0 0 1-1-1v-3.6a1 1 0 0 1 1-1Z" />129 {vol === 0 ? (130 <path d="m15.5 9.5 5 5m0-5-5 5" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" fill="none" />131 ) : (132 <>133 <path d="M15 9.2a4 4 0 0 1 0 5.6" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" fill="none" />134 {vol > 0.5 ? <path d="M17.6 6.6a7.6 7.6 0 0 1 0 10.8" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" fill="none" /> : null}135 </>136 )}137 </I>138 </button>139 <Track140 value={Math.round(vol * 100)}141 max={100}142 step={1}143 label="Volume"144 valueText={`${Math.round(vol * 100)}%`}145 onChange={(v) => {146 onVolumeChange(v / 100);147 if (muted && v > 0) onMutedChange(false);148 }}149 />150 </LiquidGlass>151 </div>152 </div>153 </GlassScope>154 );155}156 157function Skip({ back = false }: { back?: boolean }) {158 return (159 <svg viewBox="0 0 28 28" width="26" height="26" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={back ? undefined : { transform: "scaleX(-1)" }}>160 <path d="M9.5 6.5A9.5 9.5 0 1 1 4.6 15" />161 <path d="M9.8 2.6 9.5 6.5l3.9.6" />162 <text x="14" y="18" textAnchor="middle" fill="currentColor" stroke="none" fontSize="8.5" fontWeight="700" fontFamily="ui-sans-serif,-apple-system,Inter,sans-serif" style={back ? undefined : { transform: "scaleX(-1)", transformOrigin: "14px 14px" }}>163 10164 </text>165 </svg>166 );167}168 169const css = `170.fd-gmc,.fd-gmc *,.fd-gmc *::before,.fd-gmc *::after{box-sizing:border-box}171.fd-gmc{display:flex;flex-direction:column;align-items:center;gap:18px;width:100%;margin:0;padding:0;color:#fff;font:600 13px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:normal}172.fd-gmc-transport{display:flex;align-items:center;gap:22px;margin:0;padding:0}173.fd-gmc-btn{display:grid;place-items:center;width:58px;height:58px;margin:0;padding:0;color:#fff}174.fd-gmc-btn svg{display:block}175.fd-gmc-play{width:84px;height:84px}176.fd-gmc-pp{position:relative;display:block;width:34px;height:34px;margin:0;padding:0}177.fd-gmc-pp svg{position:absolute;inset:0;transition:opacity var(--fd-dur-2) var(--fd-ease-out),transform var(--fd-dur-3) var(--fd-spring)}178.fd-gmc-pp svg:first-child{transform:translateX(2px)}179.fd-gmc-pp svg:last-child{opacity:0;transform:scale(0.6)}180.fd-gmc-pp.is-playing svg:first-child{opacity:0;transform:translateX(2px) scale(0.6)}181.fd-gmc-pp.is-playing svg:last-child{opacity:1;transform:none}182.fd-gmc-bar{display:flex;align-items:center;gap:12px;width:100%;margin:0;padding:0}183.fd-gmc-scrub{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:14px;height:52px;margin:0;padding:0 20px}184.fd-gmc-vol{flex:0 0 200px;display:flex;align-items:center;gap:10px;height:52px;margin:0;padding:0 18px 0 8px}185.fd-gmc-time{flex:none;min-width:40px;margin:0;padding:0;font:600 13px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;font-variant-numeric:tabular-nums;color:rgba(255,255,255,0.92);text-align:center}186.fd-gmc-mute{flex:none;display:grid;place-items:center;width:36px;height:36px;margin:0;padding:0;border:0;border-radius:50%;background:none;color:#fff;cursor:pointer;-webkit-appearance:none;appearance:none;transition:background-color var(--fd-dur-1) linear}187.fd-gmc-mute:hover{background:rgba(255,255,255,0.12)}188.fd-gmc-mute svg{display:block}189.fd-gmc-mute:focus-visible,.fd-gmc-btn:focus-visible{outline:2px solid color-mix(in srgb,#0A84FF 70%,transparent);outline-offset:2px}190.fd-gmc-track{position:relative;flex:1 1 auto;min-width:40px;height:28px;display:block;margin:0;padding:0}191.fd-gmc-rail{position:absolute;left:0;right:0;top:50%;height:6px;margin:-3px 0 0;padding:0;display:block;border-radius:999px;background:rgba(255,255,255,0.28);overflow:hidden;transition:height var(--fd-dur-2) var(--fd-ease-out),margin var(--fd-dur-2) var(--fd-ease-out)}192.fd-gmc-track.is-active .fd-gmc-rail{height:10px;margin-top:-5px}193.fd-gmc-fill{position:absolute;left:0;top:0;bottom:0;width:calc(var(--p) * 100%);display:block;margin:0;padding:0;background:#fff;border-radius:inherit}194.fd-gmc-knob{position:absolute !important;top:50%;left:calc(var(--p) * (100% - 22px));width:22px;height:22px;margin:-11px 0 0;padding:0;display:block;pointer-events:none;195 transform:scale(calc(0.55 + var(--fd-gmc-k,0) * 0.95));--fd-lg-shadow:0 2px 6px rgba(0,0,0,0.3)}196.fd-gmc-knob-solid{position:absolute;inset:0;display:block;margin:0;padding:0;border-radius:inherit;background:#fff;opacity:calc(1 - var(--fd-gmc-k,0) * 1.15)}197.fd-gmc-knob .fd-lg-spec{opacity:var(--fd-gmc-k,0)}198.fd-gmc-input{position:absolute;inset:-8px 0;width:100%;height:calc(100% + 16px);margin:0;padding:0;border:0;opacity:0;cursor:pointer;-webkit-appearance:none;appearance:none;background:none}199.fd-gmc-track:has(.fd-gmc-input:focus-visible) .fd-gmc-rail{box-shadow:0 0 0 2px color-mix(in srgb,#0A84FF 70%,transparent)}200@media (max-width:640px){201 .fd-gmc-bar{flex-direction:column;align-items:stretch}202 .fd-gmc-vol{flex:none}203 .fd-gmc-transport{gap:16px}204}205@media (prefers-reduced-motion: reduce){.fd-gmc-pp svg{transition-duration:120ms;transform:none !important}}206`;207 208/* ───────────────────────── demo: a travel film player ───────────────────────── */209 210const DURATION = 7 * 60 + 42;211 212export function Demo() {213 const [playing, setPlaying] = useState(false);214 const [time, setTime] = useState(134);215 const [volume, setVolume] = useState(0.72);216 const [muted, setMuted] = useState(false);217 const [dim, setDim] = useState(true);218 const [shown, setShown] = useState(true);219 const stage = useRef<HTMLDivElement | null>(null);220 221 // play clock222 useEffect(() => {223 if (!playing) return;224 const id = window.setInterval(() => setTime((t) => (t + 1 >= DURATION ? 0 : t + 1)), 1000);225 return () => window.clearInterval(id);226 }, [playing]);227 228 // auto-hide chrome while playing; any pointer move brings it back229 useEffect(() => {230 if (!playing) {231 setShown(true);232 return;233 }234 let t = window.setTimeout(() => setShown(false), 3200);235 const wake = () => {236 setShown(true);237 window.clearTimeout(t);238 t = window.setTimeout(() => setShown(false), 3200);239 };240 const el = stage.current;241 el?.addEventListener("pointermove", wake);242 el?.addEventListener("keydown", wake);243 return () => {244 window.clearTimeout(t);245 el?.removeEventListener("pointermove", wake);246 el?.removeEventListener("keydown", wake);247 };248 }, [playing]);249 250 return (251 <div ref={stage} className={`fd-gmcd${playing ? " is-playing" : ""}${shown ? "" : " is-hidden"}`}>252 <style>{demoCss}</style>253 <div className="fd-gmcd-film" aria-hidden="true">254 <div className="fd-gmcd-sky" />255 <div className="fd-gmcd-sun" />256 <div className="fd-gmcd-m3" />257 <div className="fd-gmcd-m2" />258 <div className="fd-gmcd-m1" />259 <div className="fd-gmcd-lake" />260 <div className="fd-gmcd-grain" />261 </div>262 <div className="fd-gmcd-top">263 <div className="fd-gmcd-meta">264 <p className="fd-gmcd-kicker">Northbound · Episode 3</p>265 <h1 className="fd-gmcd-title">The Lofoten Light</h1>266 </div>267 <button type="button" role="switch" aria-checked={dim} className="fd-gmcd-dim" onClick={() => setDim((d) => !d)}>268 <span className="fd-gmcd-dim-dot" aria-hidden="true" />269 Dimming layer {dim ? "on" : "off"}270 </button>271 </div>272 <div className="fd-gmcd-controls">273 <GlassMediaControls274 title="Northbound, episode 3: The Lofoten Light"275 playing={playing}276 onPlayingChange={setPlaying}277 time={time}278 duration={DURATION}279 onSeek={setTime}280 volume={volume}281 onVolumeChange={setVolume}282 muted={muted}283 onMutedChange={setMuted}284 dim={dim}285 />286 </div>287 </div>288 );289}290 291const F = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;292const D = `"SF Pro Display",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Roboto,sans-serif`;293const demoCss = `294.fd-gmcd,.fd-gmcd *,.fd-gmcd *::before,.fd-gmcd *::after{box-sizing:border-box}295.fd-gmcd{position:relative;height:100vh;min-height:620px;width:100%;margin:0;padding:0;overflow:hidden;background:#0B0B0F;color:#fff;font:400 15px/1.4 ${F};letter-spacing:normal}296.fd-gmcd-film{position:absolute;inset:0;overflow:hidden}297.fd-gmcd-film > div{position:absolute;margin:0;padding:0;display:block}298.fd-gmcd-sky{inset:0;background:linear-gradient(180deg,#FDE7C8 0%,#FFC9A3 22%,#FF9E8A 40%,#E86F9A 56%,#7C5BC4 78%,#2B3A8C 100%)}299.fd-gmcd-sun{left:50%;top:44%;width:240px;height:240px;margin:-120px 0 0 -120px !important;border-radius:50%;background:radial-gradient(circle,#FFFDF2 0 22%,rgba(255,240,200,0.9) 26%,rgba(255,210,170,0.4) 46%,transparent 70%);animation:fd-gmcd-drift 22s ease-in-out infinite alternate;animation-play-state:paused}300.fd-gmcd-m3{left:-5%;right:-5%;bottom:34%;height:34%;background:#B66C9F;clip-path:polygon(0 100%,0 60%,8% 42%,14% 52%,22% 24%,30% 46%,38% 30%,47% 50%,55% 20%,63% 44%,72% 28%,80% 48%,88% 34%,100% 52%,100% 100%);opacity:0.75;animation:fd-gmcd-pan 30s linear infinite alternate;animation-play-state:paused}301.fd-gmcd-m2{left:-5%;right:-5%;bottom:30%;height:30%;background:#6F3F8A;clip-path:polygon(0 100%,0 52%,10% 30%,18% 46%,27% 12%,36% 40%,44% 22%,52% 44%,61% 8%,70% 36%,79% 24%,88% 46%,96% 30%,100% 40%,100% 100%);animation:fd-gmcd-pan 24s linear infinite alternate-reverse;animation-play-state:paused}302.fd-gmcd-m1{left:-5%;right:-5%;bottom:22%;height:24%;background:#2C1D4F;clip-path:polygon(0 100%,0 38%,7% 52%,15% 26%,24% 48%,34% 30%,42% 56%,52% 40%,60% 60%,70% 34%,78% 50%,90% 22%,100% 46%,100% 100%)}303.fd-gmcd-lake{left:0;right:0;bottom:0;height:24%;background:304 repeating-linear-gradient(180deg,rgba(255,240,225,0) 0 6px,rgba(255,240,225,0.5) 6px 7.5px,rgba(255,240,225,0) 7.5px 12px),305 linear-gradient(180deg,#4A2F78 0%,#B0679E 22%,#F29A93 55%,#FFD3A8 100%)}306.fd-gmcd-lake::before{content:"";position:absolute;left:50%;top:0;bottom:0;width:180px;margin-left:-90px;background:repeating-linear-gradient(180deg,rgba(255,244,220,0) 0 4px,rgba(255,244,220,0.85) 4px 5.5px,rgba(255,244,220,0) 5.5px 10px);307 -webkit-mask:radial-gradient(ellipse 50% 100% at 50% 0,#000 0 30%,transparent 100%);mask:radial-gradient(ellipse 50% 100% at 50% 0,#000 0 30%,transparent 100%)}308.fd-gmcd-grain{inset:0;background:radial-gradient(ellipse at 50% 40%,transparent 50%,rgba(0,0,0,0.35) 100%)}309.fd-gmcd.is-playing .fd-gmcd-sun,.fd-gmcd.is-playing .fd-gmcd-m3,.fd-gmcd.is-playing .fd-gmcd-m2{animation-play-state:running}310@keyframes fd-gmcd-drift{to{transform:translate(40px,30px)}}311@keyframes fd-gmcd-pan{to{transform:translateX(3%)}}312.fd-gmcd-top{position:absolute;left:0;right:0;top:0;display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin:0;padding:28px 32px;313 background:linear-gradient(180deg,rgba(0,0,0,0.28),transparent);transition:opacity 420ms cubic-bezier(0.22,1,0.36,1),transform 420ms cubic-bezier(0.22,1,0.36,1)}314.fd-gmcd-meta{margin:0;padding:0;text-shadow:0 1px 12px rgba(0,0,0,0.25)}315.fd-gmcd-kicker{margin:0 0 6px;font:600 13px/1.2 ${F};letter-spacing:0.08em;text-transform:uppercase;color:rgba(255,255,255,0.9)}316.fd-gmcd-title{margin:0;font:700 clamp(2rem,4.4vw,3.5rem)/1 ${D};letter-spacing:-0.035em;color:#fff}317.fd-gmcd-dim{display:inline-flex;align-items:center;gap:8px;flex:none;margin:0;padding:9px 14px;border:0;border-radius:999px;background:rgba(0,0,0,0.38);color:#fff;font:600 13px/1 ${F};cursor:pointer;-webkit-appearance:none;appearance:none;318 -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}319.fd-gmcd-dim-dot{display:block;width:10px;height:10px;border-radius:50%;background:#8E8E93;transition:background-color 240ms}320.fd-gmcd-dim[aria-checked="true"] .fd-gmcd-dim-dot{background:#30D158}321.fd-gmcd-dim:focus-visible{outline:2px solid color-mix(in srgb,#0A84FF 70%,transparent);outline-offset:2px}322.fd-gmcd-controls{position:absolute;left:50%;bottom:32px;width:min(760px,calc(100% - 32px));margin:0;padding:0;transform:translateX(-50%);transition:opacity 420ms cubic-bezier(0.22,1,0.36,1),transform 420ms cubic-bezier(0.22,1,0.36,1)}323.fd-gmcd.is-hidden .fd-gmcd-controls{opacity:0;transform:translate(-50%,16px);pointer-events:none}324.fd-gmcd.is-hidden .fd-gmcd-top{opacity:0;transform:translateY(-10px)}325.fd-gmcd.is-hidden{cursor:none}326@media (max-width:640px){327 .fd-gmcd-top{flex-direction:column;padding:20px 16px}328 .fd-gmcd-controls{bottom:20px}329}330@media (prefers-reduced-motion: reduce){.fd-gmcd-sun,.fd-gmcd-m3,.fd-gmcd-m2{animation:none}}331`;The material itself: a lens that refracts what’s behind it, with squircle bevel, specular rim, dispersion and spring physics.
glass, glassProminent, clear and clear-prominent capsules in four sizes, with a springy press and a glow from the touch point.
Floating iOS 26 tab bar: a liquid lens springs between tabs, search sits apart, and it minimizes as you scroll.