Liquid Glass
The material itself: a lens that refracts what’s behind it, with squircle bevel, specular rim, dispersion and spring physics.
An iOS 26 switch whose knob swells into clear Liquid Glass while pressed or dragged, then settles 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-toggle.json1/**2 * GlassToggle — an iOS 26 switch whose knob turns into Liquid Glass while3 * you touch it.4 *5 * At rest the knob is a solid white capsule. On press it swells, the white6 * drains away and it becomes clear glass refracting the track beneath it; you7 * can drag it across, and on release it springs to its side and turns solid8 * again (HIG: transient interactive elements may become glass).9 *10 * Props: checked, onChange(checked), disabled, label (accessible name, or11 * use aria-labelledby), accent (on colour, default #34C759), size ("m" | "l"),12 * className, id. Controlled.13 * Accessibility: <button role="switch" aria-checked>; Space/Enter toggle;14 * focus ring. Reduced motion: no swell/spring, instant snap.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, useGlassEnv, useSpring } from "./liquid-glass";20 21export interface GlassToggleProps {22 checked: boolean;23 onChange: (checked: boolean) => void;24 disabled?: boolean;25 label?: string;26 "aria-labelledby"?: string;27 "aria-describedby"?: string;28 accent?: string;29 size?: "m" | "l";30 id?: string;31 className?: string;32}33 34const DIM = { m: { w: 64, h: 28, kw: 39, kh: 24, pad: 2 }, l: { w: 80, h: 36, kw: 50, kh: 31, pad: 2.5 } };35 36export function GlassToggle({ checked, onChange, disabled, label, accent = "#34C759", size = "m", id, className, ...aria }: GlassToggleProps) {37 const env = useGlassEnv();38 const root = useRef<HTMLButtonElement | null>(null);39 const knob = useRef<HTMLElement | null>(null);40 const d = DIM[size];41 const travel = d.w - d.kw - d.pad * 2;42 const [active, setActive] = useState(false);43 44 const paint = () => {45 const k = knob.current;46 if (!k) return;47 const lift = sp.value; // 0 solid → 1 glass48 const sx = 1 + lift * 0.42 + Math.min(0.18, Math.abs(kx.velocity) * 0.0006);49 const sy = 1 + lift * 0.42 - Math.min(0.08, Math.abs(kx.velocity) * 0.0003);50 k.style.transform = `translateX(${kx.value.toFixed(2)}px) scale(${sx.toFixed(4)}, ${sy.toFixed(4)})`;51 k.style.setProperty("--fd-gtg-solid", String(Math.max(0, 1 - lift * 1.25).toFixed(3)));52 root.current?.style.setProperty("--fd-gtg-p", String((kx.value / travel).toFixed(4)));53 };54 const kx = useSpring(checked ? travel : 0, paint, { stiffness: 420, damping: 26 });55 const sp = useSpring(0, paint, { stiffness: 520, damping: 30 });56 57 // follow `checked` from outside58 const firstRun = useRef(true);59 useEffect(() => {60 if (firstRun.current || env.reducedMotion) {61 firstRun.current = false;62 kx.set(checked ? travel : 0);63 return;64 }65 if (!drag.current.active) kx.to(checked ? travel : 0);66 }, [checked, travel, env.reducedMotion, kx]);67 68 const drag = useRef({ active: false, moved: false, x0: 0, k0: 0, pid: -1 });69 70 useEffect(() => {71 const el = root.current;72 if (!el) return;73 const down = (e: PointerEvent) => {74 if (disabled || e.button !== 0) return;75 const g = drag.current;76 g.active = true;77 g.moved = false;78 g.x0 = e.clientX;79 g.k0 = kx.value;80 g.pid = e.pointerId;81 el.setPointerCapture(e.pointerId);82 setActive(true);83 if (env.reducedMotion) sp.set(1);84 else sp.to(1);85 };86 const move = (e: PointerEvent) => {87 const g = drag.current;88 if (!g.active || e.pointerId !== g.pid) return;89 const dx = e.clientX - g.x0;90 if (Math.abs(dx) > 3) g.moved = true;91 if (!g.moved) return;92 let x = g.k0 + dx;93 if (x < 0) x *= 0.25;94 if (x > travel) x = travel + (x - travel) * 0.25;95 kx.stiffness = 1000;96 kx.damping = 55;97 kx.to(x);98 };99 const up = (e: PointerEvent) => {100 const g = drag.current;101 if (!g.active || e.pointerId !== g.pid) return;102 g.active = false;103 setActive(false);104 kx.stiffness = 420;105 kx.damping = 26;106 const next = g.moved ? kx.value > travel / 2 : !checked;107 if (env.reducedMotion) {108 sp.set(0);109 kx.set(next ? travel : 0);110 } else {111 sp.to(0);112 kx.to(next ? travel : 0);113 }114 if (next !== checked) onChange(next);115 };116 el.addEventListener("pointerdown", down);117 el.addEventListener("pointermove", move);118 el.addEventListener("pointerup", up);119 el.addEventListener("pointercancel", up);120 return () => {121 el.removeEventListener("pointerdown", down);122 el.removeEventListener("pointermove", move);123 el.removeEventListener("pointerup", up);124 el.removeEventListener("pointercancel", up);125 };126 }, [checked, disabled, onChange, travel, env.reducedMotion, kx, sp]);127 128 return (129 <GlassScope>130 <button131 ref={root}132 id={id}133 type="button"134 role="switch"135 aria-checked={checked}136 aria-label={label}137 disabled={disabled}138 className={["fd-gtg", `fd-gtg--${size}`, active ? "is-active" : "", className ?? ""].filter(Boolean).join(" ")}139 style={{ "--fd-gtg-accent": accent, "--fd-gtg-w": `${d.w}px`, "--fd-gtg-h": `${d.h}px`, "--fd-gtg-kw": `${d.kw}px`, "--fd-gtg-kh": `${d.kh}px`, "--fd-gtg-pad": `${d.pad}px`, "--fd-gtg-p": checked ? 1 : 0 } as CSSProperties}140 onClick={(e) => {141 // pointer toggles are handled on pointerup; this is keyboard / AT activation142 if (e.detail === 0 && !disabled) onChange(!checked);143 }}144 {...aria}145 >146 <style>{css}</style>147 <span className="fd-gtg-track" aria-hidden="true">148 <span className="fd-gtg-on" />149 </span>150 <LiquidGlass ref={knob} as="span" className="fd-gtg-knob" variant="clear" radius="capsule" refraction={1.25} dispersion={0.1} shadow={false} aria-hidden="true">151 <span className="fd-gtg-solid" />152 </LiquidGlass>153 </button>154 </GlassScope>155 );156}157 158const css = `159.fd-gtg,.fd-gtg *,.fd-gtg *::before,.fd-gtg *::after{box-sizing:border-box}160.fd-gtg{position:relative;display:inline-block;flex:none;width:var(--fd-gtg-w);height:var(--fd-gtg-h);margin:0;padding:0;border:0;border-radius:999px;background:none;color:inherit;font:inherit;vertical-align:middle;161 cursor:pointer;touch-action:none;-webkit-appearance:none;appearance:none;-webkit-tap-highlight-color:transparent;outline:none}162.fd-gtg:focus-visible{outline:2px solid color-mix(in srgb,#0A84FF 70%,transparent);outline-offset:2px}163.fd-gtg:disabled{cursor:default;opacity:0.45}164.fd-gtg-track{position:absolute;inset:0;display:block;margin:0;padding:0;border-radius:inherit;overflow:hidden;background:rgba(120,120,128,0.16);box-shadow:inset 0 0 0 0.5px rgba(0,0,0,0.04)}165.fd-gtg-on{position:absolute;inset:0;display:block;margin:0;padding:0;border-radius:inherit;background:var(--fd-gtg-accent);opacity:var(--fd-gtg-p);transform-origin:0 50%}166.fd-gtg-knob{position:absolute !important;left:var(--fd-gtg-pad);top:var(--fd-gtg-pad);display:block;width:var(--fd-gtg-kw);height:var(--fd-gtg-kh);margin:0;padding:0;will-change:transform;167 --fd-lg-shadow:0 3px 8px rgba(0,0,0,0.15),0 1px 1px rgba(0,0,0,0.12),0 3px 1px rgba(0,0,0,0.04)}168.fd-gtg.is-active .fd-gtg-knob{--fd-lg-shadow:0 8px 22px -6px rgba(0,0,0,0.28),0 1px 2px rgba(0,0,0,0.08)}169.fd-gtg-solid{position:absolute;inset:0;display:block;margin:0;padding:0;border-radius:inherit;background:#FFFFFF;opacity:var(--fd-gtg-solid,1)}170.fd-gtg-knob .fd-lg-spec{opacity:calc(1 - var(--fd-gtg-solid,1) * 0.85)}171.fd-gtg-knob .fd-lg-fill{background:rgba(255,255,255,0.04)}172@media (prefers-contrast: more){.fd-gtg-track{box-shadow:inset 0 0 0 1px rgba(0,0,0,0.6)}.fd-gtg-solid{opacity:1 !important}}173@media (prefers-reduced-transparency: reduce){.fd-gtg-solid{opacity:1 !important}}174`;175 176/* ───────────────────────── demo: smart-home settings ───────────────────────── */177 178type Row = { id: string; title: string; sub: string; icon: ReactNode; color: string; on: boolean; accent?: string };179 180const svg = (d: ReactNode) => (181 <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">182 {d}183 </svg>184);185 186const INITIAL: Row[] = [187 { id: "away", title: "Away mode", sub: "Lights follow your usual evening pattern", color: "#5856D6", on: true, icon: svg(<path d="M4 20V9l8-5 8 5v11M9 20v-6h6v6" />) },188 { id: "heat", title: "Underfloor heating", sub: "Bathroom & hallway · 21.5 °C", color: "#FF9500", on: true, icon: svg(<path d="M8 4c-2 3 2 5 0 8s2 5 0 8M16 4c-2 3 2 5 0 8s2 5 0 8" />) },189 { id: "blinds", title: "Sunrise blinds", sub: "Open gradually from 06:40", color: "#FF2D55", on: false, icon: svg(<><circle cx="12" cy="15" r="4" /><path d="M3 15h2M19 15h2M12 3v4M5.6 8.6l1.4 1.4M18.4 8.6 17 10M3 20h18" /></>) },190 { id: "leak", title: "Leak alerts", sub: "Kitchen, laundry, water heater", color: "#0A84FF", on: true, icon: svg(<path d="M12 3.5s6 6.4 6 10.5a6 6 0 0 1-12 0c0-4.1 6-10.5 6-10.5Z" />) },191 { id: "solar", title: "Charge car from solar only", sub: "Wait for > 3.2 kW surplus", color: "#34C759", on: false, icon: svg(<><rect x="3" y="6" width="18" height="11" rx="2" /><path d="M3 11.5h18M9 6v11M15 6v11M12 17v3" /></>) },192];193 194export function Demo() {195 const [rows, setRows] = useState(INITIAL);196 const [night, setNight] = useState(true);197 const set = (id: string, v: boolean) => setRows((r) => r.map((x) => (x.id === id ? { ...x, on: v } : x)));198 const on = rows.filter((r) => r.on).length;199 return (200 <div className="fd-gtgd">201 <style>{demoCss}</style>202 <div className="fd-gtgd-wrap">203 <header className="fd-gtgd-head">204 <p className="fd-gtgd-eyebrow">Haus Lindenhof · 4 rooms</p>205 <h1 className="fd-gtgd-h">Automations</h1>206 <p className="fd-gtgd-sub">207 {on} of {rows.length} running · last change 18:42208 </p>209 </header>210 211 <section className="fd-gtgd-feature">212 <div className="fd-gtgd-feature-art" aria-hidden="true" />213 <div className="fd-gtgd-feature-body">214 <p className="fd-gtgd-feature-t" id="fd-gtgd-night">215 Night scene216 </p>217 <p className="fd-gtgd-feature-s">Dims 14 lights to 8%, locks the front door, arms the garden cameras at 23:00.</p>218 </div>219 <GlassToggle size="l" checked={night} onChange={setNight} aria-labelledby="fd-gtgd-night" accent="#FF9F0A" />220 </section>221 222 <ul className="fd-gtgd-list" aria-label="Automations">223 {rows.map((r) => (224 <li key={r.id} className="fd-gtgd-row">225 <span className="fd-gtgd-ic" style={{ background: r.color }}>226 {r.icon}227 </span>228 <span className="fd-gtgd-txt">229 <span className="fd-gtgd-t" id={`fd-gtgd-${r.id}`}>230 {r.title}231 </span>232 <span className="fd-gtgd-s" id={`fd-gtgd-${r.id}-s`}>233 {r.sub}234 </span>235 </span>236 <GlassToggle checked={r.on} onChange={(v) => set(r.id, v)} aria-labelledby={`fd-gtgd-${r.id}`} aria-describedby={`fd-gtgd-${r.id}-s`} />237 </li>238 ))}239 <li className="fd-gtgd-row">240 <span className="fd-gtgd-ic" style={{ background: "#8E8E93" }}>241 {svg(<><rect x="5" y="10" width="14" height="10" rx="2" /><path d="M8 10V7a4 4 0 0 1 8 0v3" /></>)}242 </span>243 <span className="fd-gtgd-txt">244 <span className="fd-gtgd-t" id="fd-gtgd-lock">245 Auto-lock garage246 </span>247 <span className="fd-gtgd-s">Requires the Hallo Gate hub</span>248 </span>249 <GlassToggle checked={false} onChange={() => {}} disabled aria-labelledby="fd-gtgd-lock" />250 </li>251 </ul>252 <p className="fd-gtgd-foot">Press and hold a switch — the knob turns to glass. Drag it across.</p>253 </div>254 </div>255 );256}257 258const F = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;259const D = `"SF Pro Display",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Roboto,sans-serif`;260const demoCss = `261.fd-gtgd,.fd-gtgd *,.fd-gtgd *::before,.fd-gtgd *::after{box-sizing:border-box}262.fd-gtgd{min-height:100vh;width:100%;margin:0;padding:56px 20px;display:flex;justify-content:center;align-items:flex-start;263 background:radial-gradient(900px 500px at 15% -10%,#FFE8CC 0%,transparent 60%),radial-gradient(800px 500px at 110% 20%,#DDE7FF 0%,transparent 55%),#F2F2F7;color:#0B0B0F;font:400 15px/1.4 ${F};letter-spacing:normal}264.fd-gtgd-wrap{width:100%;max-width:560px;margin:0;padding:0}265.fd-gtgd-head{margin:0 0 20px;padding:0 4px}266.fd-gtgd-eyebrow{margin:0 0 4px;font:600 13px/1.3 ${F};letter-spacing:0.02em;text-transform:uppercase;color:#6B6B76}267.fd-gtgd-h{margin:0 0 4px;font:800 2.75rem/1.05 ${D};letter-spacing:-0.035em;color:#0B0B0F}268.fd-gtgd-sub{margin:0;font:500 15px/1.4 ${F};color:#6B6B76}269.fd-gtgd-feature{display:flex;align-items:center;gap:16px;margin:0 0 16px;padding:14px 18px 14px 14px;border-radius:24px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.04),0 12px 32px -16px rgba(20,20,40,0.18)}270.fd-gtgd-feature-art{flex:none;width:64px;height:64px;margin:0;border-radius:16px;background:271 radial-gradient(circle at 70% 30%,#FFF1B8 0 10%,transparent 11%),radial-gradient(circle at 30% 70%,rgba(255,160,40,0.9) 0 22%,transparent 50%),272 linear-gradient(160deg,#1B1F4A,#3A2A6B 60%,#FF9F0A 140%)}273.fd-gtgd-feature-body{flex:1;min-width:0;margin:0;padding:0}274.fd-gtgd-feature-t{margin:0 0 2px;font:700 17px/1.25 ${F};letter-spacing:-0.01em;color:#0B0B0F}275.fd-gtgd-feature-s{margin:0;font:400 13px/1.4 ${F};color:#6B6B76}276.fd-gtgd-list{list-style:none;margin:0;padding:6px 0;border-radius:24px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.04),0 12px 32px -16px rgba(20,20,40,0.18)}277.fd-gtgd-row{position:relative;display:flex;align-items:center;gap:14px;margin:0;padding:12px 18px 12px 14px;min-height:64px}278.fd-gtgd-row + .fd-gtgd-row::before{content:"";position:absolute;left:62px;right:0;top:0;height:0.5px;background:#E2E2E8}279.fd-gtgd-ic{flex:none;display:grid;place-items:center;width:34px;height:34px;margin:0;border-radius:9px;color:#fff}280.fd-gtgd-ic svg{display:block}281.fd-gtgd-txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;margin:0;padding:0}282.fd-gtgd-t{display:block;font:600 16px/1.3 ${F};letter-spacing:-0.01em;color:#0B0B0F}283.fd-gtgd-s{display:block;font:400 13px/1.35 ${F};color:#6B6B76}284.fd-gtgd-foot{margin:16px 0 0;padding:0 4px;font:400 13px/1.4 ${F};color:#8A8A94;text-align:center}285@media (max-width:480px){.fd-gtgd{padding:32px 16px}.fd-gtgd-h{font-size:2.25rem}}286`;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.