Liquid Glass
The material itself: a lens that refracts what’s behind it, with squircle bevel, specular rim, dispersion and spring physics.
A slider whose thumb becomes refracting glass while dragged, stretching along its velocity and springing back to solid.
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-slider.json1/**2 * GlassSlider — a slider whose thumb turns into Liquid Glass while dragged.3 *4 * At rest the thumb is a compact solid capsule. Grab it and it swells into5 * clear glass that refracts the fill and track underneath, stretches along6 * its velocity (area-preserving) and springs back to solid on release.7 *8 * Props: value, onChange(value), min (0), max (100), step (1), label9 * (accessible name) or aria-labelledby, valueText(value) for10 * aria-valuetext, accent (fill colour or CSS gradient), tone ("light" |11 * "dark"), ticks (number of tick marks), disabled, className.12 * Accessibility: native <input type="range"> drives everything — Arrow keys,13 * PageUp/PageDown, Home/End and screen readers work as usual.14 * Reduced motion: no swell or stretch, thumb moves instantly.15 * Browser support: refraction in Chromium; frosted glass elsewhere.16 */17import { useEffect, useRef, useState } from "react";18import type { CSSProperties, ReactNode } from "react";19import { GlassScope, LiquidGlass, createVelocityTracker, useGlassEnv, useSpring } from "./liquid-glass";20 21export interface GlassSliderProps {22 value: number;23 onChange: (value: number) => void;24 min?: number;25 max?: number;26 step?: number;27 label?: string;28 "aria-labelledby"?: string;29 valueText?: (v: number) => string;30 /** Fill colour or gradient. */31 accent?: string;32 tone?: "light" | "dark";33 ticks?: number;34 disabled?: boolean;35 className?: string;36 id?: string;37}38 39const KW = 38; // thumb width at rest40const KH = 24;41 42export function GlassSlider({43 value,44 onChange,45 min = 0,46 max = 100,47 step = 1,48 label,49 valueText,50 accent = "#0A84FF",51 tone = "light",52 ticks = 0,53 disabled,54 className,55 id,56 ...aria57}: GlassSliderProps) {58 const env = useGlassEnv();59 const root = useRef<HTMLDivElement | null>(null);60 const thumb = useRef<HTMLElement | null>(null);61 const input = useRef<HTMLInputElement | null>(null);62 const [active, setActive] = useState(false);63 const pct = (value - min) / (max - min || 1);64 65 const width = useRef(0);66 const paint = () => {67 const t = thumb.current;68 const r = root.current;69 if (!t || !r) return;70 const lift = sp.value;71 const v = env.reducedMotion ? 0 : px.velocity;72 const st = Math.min(0.32, Math.abs(v) * 0.0005);73 const s = 1 + lift * 0.55;74 const x = px.value * (width.current - KW);75 t.style.transform = `translateX(${x.toFixed(2)}px) scale(${(s * (1 + st)).toFixed(4)}, ${(s / (1 + st * 0.7)).toFixed(4)})`;76 t.style.setProperty("--fd-gsl-solid", Math.max(0, 1 - lift * 1.3).toFixed(3));77 r.style.setProperty("--fd-gsl-x", `${(x + KW / 2).toFixed(2)}px`);78 };79 const px = useSpring(pct, paint, { stiffness: 520, damping: 34, precision: 0.0002 });80 const sp = useSpring(0, paint, { stiffness: 520, damping: 30 });81 82 // measure + follow external value changes83 useEffect(() => {84 const r = root.current;85 if (!r) return;86 const measure = () => {87 width.current = r.clientWidth;88 paint();89 };90 measure();91 const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(measure) : null;92 ro?.observe(r);93 return () => ro?.disconnect();94 // eslint-disable-next-line react-hooks/exhaustive-deps95 }, []);96 const dragging = useRef(false);97 useEffect(() => {98 if (dragging.current) return;99 if (env.reducedMotion) px.set(pct);100 else px.to(pct);101 }, [pct, env.reducedMotion, px]);102 103 // latest props for the long-lived pointer handlers104 const live = useRef({ value, min, max, step, disabled, onChange });105 live.current = { value, min, max, step, disabled, onChange };106 107 // pointer: drag anywhere on the track; thumb follows the finger continuously, value snaps to step108 useEffect(() => {109 const r = root.current;110 if (!r) return;111 const vt = createVelocityTracker();112 let pid = -1;113 let grabOffset = 0;114 const toPct = (clientX: number) => {115 const b = r.getBoundingClientRect();116 return Math.min(1, Math.max(0, (clientX - b.left - grabOffset - KW / 2) / (b.width - KW)));117 };118 const emit = (p: number) => {119 const { value, min, max, step, onChange } = live.current;120 const raw = min + p * (max - min);121 const snapped = Math.min(max, Math.max(min, Math.round((raw - min) / step) * step + min));122 if (snapped !== value) onChange(Number(snapped.toFixed(6)));123 };124 const down = (e: PointerEvent) => {125 if (live.current.disabled || e.button !== 0) return;126 e.preventDefault();127 pid = e.pointerId;128 r.setPointerCapture(pid);129 r.dataset.pointer = "1";130 input.current?.focus({ preventScroll: true });131 const b = r.getBoundingClientRect();132 const thumbX = b.left + px.value * (b.width - KW) + KW / 2;133 // grabbing the thumb keeps the offset; tapping the track jumps there134 grabOffset = Math.abs(e.clientX - thumbX) < KW ? e.clientX - thumbX : 0;135 dragging.current = true;136 setActive(true);137 vt.reset();138 vt.push(e.clientX, 0);139 if (env.reducedMotion) sp.set(1);140 else sp.to(1);141 const p = toPct(e.clientX);142 px.stiffness = 700;143 px.damping = 40;144 if (env.reducedMotion) px.set(p);145 else px.to(p);146 emit(p);147 };148 const move = (e: PointerEvent) => {149 if (e.pointerId !== pid) return;150 vt.push(e.clientX, 0);151 const p = toPct(e.clientX);152 if (env.reducedMotion) px.set(p);153 else px.to(p);154 emit(p);155 };156 const up = (e: PointerEvent) => {157 if (e.pointerId !== pid) return;158 pid = -1;159 dragging.current = false;160 setActive(false);161 px.stiffness = 520;162 px.damping = 34;163 if (env.reducedMotion) sp.set(0);164 else sp.to(0);165 };166 r.addEventListener("pointerdown", down);167 r.addEventListener("pointermove", move);168 r.addEventListener("pointerup", up);169 r.addEventListener("pointercancel", up);170 return () => {171 r.removeEventListener("pointerdown", down);172 r.removeEventListener("pointermove", move);173 r.removeEventListener("pointerup", up);174 r.removeEventListener("pointercancel", up);175 };176 }, [env.reducedMotion, px, sp]);177 178 // after a drag ends, glide to the snapped position179 useEffect(() => {180 if (!active && !dragging.current) px.to(pct);181 }, [active, pct, px]);182 183 return (184 <GlassScope>185 <div186 ref={root}187 className={["fd-gsl", `fd-gsl--${tone}`, active ? "is-active" : "", disabled ? "is-disabled" : "", className ?? ""].filter(Boolean).join(" ")}188 style={{ "--fd-gsl-accent": accent, "--fd-gsl-kw": `${KW}px`, "--fd-gsl-kh": `${KH}px`, "--fd-gsl-x": `calc(${pct} * (100% - ${KW}px) + ${KW / 2}px)` } as CSSProperties}189 >190 <style>{css}</style>191 <span className="fd-gsl-track" aria-hidden="true">192 <span className="fd-gsl-fill" />193 {ticks > 1 && (194 <span className="fd-gsl-ticks">195 {Array.from({ length: ticks }, (_, i) => (196 <i key={i} />197 ))}198 </span>199 )}200 </span>201 <LiquidGlass ref={thumb} as="span" className="fd-gsl-thumb" variant="clear" radius="capsule" refraction={1.3} dispersion={0.1} shadow={false} aria-hidden="true">202 <span className="fd-gsl-solid" />203 </LiquidGlass>204 <input205 ref={input}206 id={id}207 className="fd-gsl-input"208 type="range"209 min={min}210 max={max}211 step={step}212 value={value}213 disabled={disabled}214 aria-label={label}215 aria-valuetext={valueText?.(value)}216 onChange={(e) => onChange(Number(e.target.value))}217 onKeyDown={() => root.current?.removeAttribute("data-pointer")}218 {...aria}219 />220 </div>221 </GlassScope>222 );223}224 225const css = `226.fd-gsl,.fd-gsl *,.fd-gsl *::before,.fd-gsl *::after{box-sizing:border-box}227.fd-gsl{--fd-gsl-track:rgba(120,120,128,0.2);position:relative;display:block;width:100%;height:36px;margin:0;padding:0;touch-action:none;cursor:pointer;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}228.fd-gsl--dark{--fd-gsl-track:rgba(255,255,255,0.18)}229.fd-gsl.is-disabled{opacity:0.45;cursor:default}230.fd-gsl-track{position:absolute;left:0;right:0;top:50%;height:6px;margin:-3px 0 0;padding:0;border-radius:999px;background:var(--fd-gsl-track);overflow:hidden;display:block}231.fd-gsl-fill{position:absolute;left:0;top:0;bottom:0;width:var(--fd-gsl-x);display:block;margin:0;padding:0;border-radius:inherit;background:var(--fd-gsl-accent)}232.fd-gsl-ticks{position:absolute;inset:0;display:flex;justify-content:space-between;align-items:center;margin:0;padding:0 calc(var(--fd-gsl-kw) / 2 - 1px)}233.fd-gsl-ticks i{display:block;width:2px;height:2px;margin:0;border-radius:2px;background:rgba(255,255,255,0.7)}234.fd-gsl-thumb{position:absolute !important;left:0;top:50%;display:block;width:var(--fd-gsl-kw);height:var(--fd-gsl-kh);margin:calc(var(--fd-gsl-kh) / -2) 0 0;padding:0;pointer-events:none;will-change:transform;235 --fd-lg-shadow:0 3px 8px rgba(0,0,0,0.16),0 1px 1px rgba(0,0,0,0.12)}236.fd-gsl.is-active .fd-gsl-thumb{--fd-lg-shadow:0 10px 24px -8px rgba(0,0,0,0.3)}237.fd-gsl-solid{position:absolute;inset:0;display:block;margin:0;padding:0;border-radius:inherit;background:#FFFFFF;opacity:var(--fd-gsl-solid,1)}238.fd-gsl-thumb .fd-lg-spec{opacity:calc(1 - var(--fd-gsl-solid,1) * 0.85)}239.fd-gsl-thumb .fd-lg-fill{background:rgba(255,255,255,0.04)}240.fd-gsl-input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;opacity:0;pointer-events:none;-webkit-appearance:none;appearance:none;background:none}241.fd-gsl:not([data-pointer]):has(.fd-gsl-input:focus-visible) .fd-gsl-thumb{outline:2px solid color-mix(in srgb,#0A84FF 70%,transparent);outline-offset:3px}242@media (prefers-contrast: more){.fd-gsl-track{box-shadow:inset 0 0 0 1px rgba(0,0,0,0.6)}.fd-gsl-solid{opacity:1 !important}}243@media (prefers-reduced-transparency: reduce){.fd-gsl-solid{opacity:1 !important}}244`;245 246/* ───────────────────────── demo: EV climate controls ───────────────────────── */247 248const Ico = ({ children }: { children: ReactNode }) => (249 <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">250 {children}251 </svg>252);253 254export function Demo() {255 const [temp, setTemp] = useState(21.5);256 const [fan, setFan] = useState(3);257 const [seat, setSeat] = useState(2);258 const [charge, setCharge] = useState(80);259 const warm = (temp - 16) / 12; // 0..1260 const range = Math.round(charge * 5.12);261 return (262 <div className="fd-gsld" style={{ "--warm": warm } as CSSProperties}>263 <style>{demoCss}</style>264 <div className="fd-gsld-glow" aria-hidden="true" />265 <div className="fd-gsld-car" aria-hidden="true">266 <svg viewBox="0 0 600 220" width="100%" height="100%" preserveAspectRatio="xMidYMid meet">267 <defs>268 <linearGradient id="fdgsldbody" x1="0" y1="0" x2="0" y2="1">269 <stop offset="0" stopColor="#3A3A44" />270 <stop offset="0.5" stopColor="#1C1C22" />271 <stop offset="1" stopColor="#0B0B0F" />272 </linearGradient>273 <linearGradient id="fdgsldglass" x1="0" y1="0" x2="1" y2="1">274 <stop offset="0" stopColor="#8EC5FF" stopOpacity="0.55" />275 <stop offset="1" stopColor="#1B2A44" stopOpacity="0.9" />276 </linearGradient>277 </defs>278 <path d="M40 150c6-30 40-44 90-50l70-46c22-14 46-20 80-20h70c40 0 70 10 100 32l46 34c50 6 82 18 90 46l4 22c0 10-8 16-18 16H58c-12 0-20-6-20-16Z" fill="url(#fdgsldbody)" />279 <path d="M210 64c18-12 38-16 66-16h52v52H160Z" fill="url(#fdgsldglass)" />280 <path d="M346 48h20c34 0 58 8 84 26l30 26H346Z" fill="url(#fdgsldglass)" />281 <path d="M60 140h480" stroke="rgba(255,255,255,0.12)" strokeWidth="2" />282 <circle cx="150" cy="180" r="38" fill="#0B0B0F" />283 <circle cx="150" cy="180" r="24" fill="#3A3A44" stroke="#6B6B76" strokeWidth="3" />284 <circle cx="458" cy="180" r="38" fill="#0B0B0F" />285 <circle cx="458" cy="180" r="24" fill="#3A3A44" stroke="#6B6B76" strokeWidth="3" />286 <rect x="530" y="128" width="26" height="6" rx="3" fill="#FF453A" />287 <rect x="44" y="124" width="30" height="6" rx="3" fill="#F2F2F7" />288 </svg>289 </div>290 291 <header className="fd-gsld-head">292 <p className="fd-gsld-eyebrow">Voltra GT · Pre-conditioning</p>293 <h1 className="fd-gsld-temp">294 {temp.toFixed(1)}295 <span>°C</span>296 </h1>297 <p className="fd-gsld-sub">Cabin ready by 07:45 · 61% · {range} km range</p>298 </header>299 300 <section className="fd-gsld-panel" aria-label="Climate">301 <div className="fd-gsld-row">302 <label className="fd-gsld-lbl" id="fd-gsld-t">303 <Ico>304 <path d="M10 4a2 2 0 0 1 4 0v9.5a4 4 0 1 1-4 0Z" />305 </Ico>306 Temperature307 </label>308 <span className="fd-gsld-val">{temp.toFixed(1)} °C</span>309 <GlassSlider310 value={temp}311 onChange={setTemp}312 min={16}313 max={28}314 step={0.5}315 aria-labelledby="fd-gsld-t"316 valueText={(v) => `${v.toFixed(1)} degrees Celsius`}317 tone="dark"318 accent="linear-gradient(90deg,#0A84FF,#64D2FF 40%,#FFD60A 70%,#FF453A)"319 />320 </div>321 <div className="fd-gsld-row">322 <label className="fd-gsld-lbl" id="fd-gsld-f">323 <Ico>324 <circle cx="12" cy="12" r="1.8" />325 <path d="M12 10.2c0-4 1-6.7 3.4-6.7 2.6 0 2.8 3.6.2 5.6M13.8 12c4 0 6.7 1 6.7 3.4 0 2.6-3.6 2.8-5.6.2M12 13.8c0 4-1 6.7-3.4 6.7-2.6 0-2.8-3.6-.2-5.6M10.2 12c-4 0-6.7-1-6.7-3.4 0-2.6 3.6-2.8 5.6-.2" />326 </Ico>327 Fan328 </label>329 <span className="fd-gsld-val">{fan === 0 ? "Off" : `Level ${fan}`}</span>330 <GlassSlider value={fan} onChange={setFan} min={0} max={7} step={1} ticks={8} aria-labelledby="fd-gsld-f" valueText={(v) => (v === 0 ? "Off" : `Level ${v} of 7`)} tone="dark" accent="#64D2FF" />331 </div>332 <div className="fd-gsld-row">333 <label className="fd-gsld-lbl" id="fd-gsld-s">334 <Ico>335 <path d="M7 3.5v8.5a3 3 0 0 0 3 3h7l2 5.5M7 9h4" />336 <path d="M14 3.5c-1 1.3 1 2.2 0 3.5M17 3.5c-1 1.3 1 2.2 0 3.5" />337 </Ico>338 Seat heating339 </label>340 <span className="fd-gsld-val">{["Off", "Low", "Medium", "High"][seat]}</span>341 <GlassSlider value={seat} onChange={setSeat} min={0} max={3} step={1} ticks={4} aria-labelledby="fd-gsld-s" valueText={(v) => ["Off", "Low", "Medium", "High"][v]} tone="dark" accent="#FF9F0A" />342 </div>343 <div className="fd-gsld-row">344 <label className="fd-gsld-lbl" id="fd-gsld-c">345 <Ico>346 <rect x="3" y="7" width="16" height="10" rx="2.5" />347 <path d="M21 10.5v3M11.5 9 9 12.5h4L10.5 16" />348 </Ico>349 Charge limit350 </label>351 <span className="fd-gsld-val">{charge}%</span>352 <GlassSlider value={charge} onChange={setCharge} min={50} max={100} step={5} aria-labelledby="fd-gsld-c" valueText={(v) => `${v} percent, about ${Math.round(v * 5.12)} kilometres`} tone="dark" accent="#30D158" />353 </div>354 </section>355 </div>356 );357}358 359const F = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;360const D = `"SF Pro Display",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Roboto,sans-serif`;361const demoCss = `362.fd-gsld,.fd-gsld *,.fd-gsld *::before,.fd-gsld *::after{box-sizing:border-box}363.fd-gsld{position:relative;min-height:100vh;width:100%;margin:0;padding:48px 24px;display:grid;grid-template-columns:minmax(0,600px) minmax(0,440px);grid-template-areas:"head panel" "car panel";gap:24px 56px;align-content:center;justify-content:center;overflow:hidden;364 background:#07080C;color:#F2F2F7;font:400 15px/1.4 ${F};letter-spacing:normal}365.fd-gsld-glow{position:absolute;inset:-20%;pointer-events:none;background:366 radial-gradient(40% 40% at 30% 60%,color-mix(in srgb,#0A84FF calc((1 - var(--warm)) * 70%),#FF6A3D calc(var(--warm) * 70%)) 0%,transparent 70%),367 radial-gradient(30% 30% at 80% 20%,rgba(100,210,255,0.18),transparent 70%);opacity:0.75;transition:background 420ms}368.fd-gsld-head{grid-area:head;position:relative;align-self:end;margin:0;padding:0}369.fd-gsld-eyebrow{margin:0 0 6px;font:600 13px/1.3 ${F};letter-spacing:0.06em;text-transform:uppercase;color:#8E8E99}370.fd-gsld-temp{margin:0;font:200 clamp(5.5rem,11vw,8.5rem)/0.9 ${D};letter-spacing:-0.05em;color:#fff;font-variant-numeric:tabular-nums}371.fd-gsld-temp span{font:300 0.32em/1 ${D};letter-spacing:-0.01em;vertical-align:top;margin-left:6px;color:#AEAEB8}372.fd-gsld-sub{margin:10px 0 0;font:500 15px/1.4 ${F};color:#AEAEB8}373.fd-gsld-car{grid-area:car;position:relative;max-width:620px;height:220px;margin:0;opacity:0.95}374.fd-gsld-car svg{display:block}375.fd-gsld-panel{grid-area:panel;position:relative;align-self:center;margin:0;padding:8px 22px;border-radius:28px;background:rgba(28,28,34,0.82);box-shadow:inset 0 0 0 0.5px rgba(255,255,255,0.1),0 30px 60px -30px rgba(0,0,0,0.8)}376.fd-gsld-row{display:grid;grid-template-columns:1fr auto;grid-template-areas:"l v" "s s";gap:4px 12px;margin:0;padding:16px 0}377.fd-gsld-row + .fd-gsld-row{border-top:0.5px solid rgba(255,255,255,0.1)}378.fd-gsld-lbl{grid-area:l;display:flex;align-items:center;gap:10px;margin:0;padding:0;font:600 15px/1.3 ${F};letter-spacing:-0.005em;color:#F2F2F7;cursor:default}379.fd-gsld-lbl svg{display:block;color:#AEAEB8}380.fd-gsld-val{grid-area:v;font:500 15px/1.3 ${F};color:#AEAEB8;font-variant-numeric:tabular-nums}381.fd-gsld-row .fd-gsl{grid-area:s}382@media (max-width:900px){383 .fd-gsld{grid-template-columns:minmax(0,1fr);grid-template-areas:"head" "car" "panel";gap:12px;padding:32px 16px;align-content:start}384 .fd-gsld-car{height:120px;max-width:none}385 .fd-gsld-panel{padding:4px 18px}386}387`;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.