Glass Button

glass, glassProminent, clear and clear-prominent capsules in four sizes, with a springy press and a glow from the touch point.

  • Liquid Glass
  • Travel & hospitality
  • 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-button.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/liquid-glass/glass-button.tsx
1/**
2 * GlassButton — Liquid Glass buttons from Apple's HIG.
3 *
4 * Styles: "glass" regular glass, neutral (secondary actions)
5 * "glassProminent" glass tinted with the accent (the primary action)
6 * "clear" clear glass for photos/video (+ `dim` over bright media)
7 * "clearProminent" clear glass with accent tint
8 * Sizes: "s" 32 · "m" 40 · "l" 48 · "xl" 60 (capsules; icon-only buttons are circles)
9 *
10 * Props: glass, size, tone ("light" | "dark", regular glass only), tint (accent,
11 * default #007AFF), icon (leading icon node), iconOnly (requires aria-label),
12 * dim, plus every native <button> attribute.
13 * Wrap many buttons in <GlassScope> (from ./liquid-glass) to render the
14 * stylesheets once.
15 *
16 * Behaviour: press scales to 0.96 and releases with a spring overshoot, and a
17 * soft glow blooms from the touch point (keyboard: from the centre).
18 * Browser support: real refraction in Chromium; frosted fallback elsewhere
19 * (see ./liquid-glass). Accessibility: native <button>, visible focus ring,
20 * disabled uses the native attribute. Icon-only buttons must get aria-label.
21 */
22import { forwardRef, useState } from "react";
23import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from "react";
24import { GlassScope, LiquidGlass } from "./liquid-glass";
25
26export type GlassButtonStyle = "glass" | "glassProminent" | "clear" | "clearProminent";
27export type GlassButtonSize = "s" | "m" | "l" | "xl";
28
29export interface GlassButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
30 glass?: GlassButtonStyle;
31 size?: GlassButtonSize;
32 tone?: "light" | "dark";
33 /** Accent used by the prominent styles. */
34 tint?: string;
35 icon?: ReactNode;
36 iconOnly?: boolean;
37 /** Clear styles: 35% dimming layer for bright backgrounds. */
38 dim?: boolean;
39}
40
41const HEIGHT: Record<GlassButtonSize, number> = { s: 32, m: 40, l: 48, xl: 60 };
42
43export const GlassButton = forwardRef<HTMLButtonElement, GlassButtonProps>(function GlassButton(
44 { glass = "glass", size = "m", tone = "light", tint = "#007AFF", icon, iconOnly = false, dim = false, className, style, children, type = "button", ...rest },
45 ref,
46) {
47 const clear = glass === "clear" || glass === "clearProminent";
48 const prominent = glass === "glassProminent" || glass === "clearProminent";
49 const h = HEIGHT[size];
50 return (
51 <LiquidGlass
52 ref={ref as never}
53 as="button"
54 type={type}
55 variant={clear ? "clear" : "regular"}
56 tone={tone}
57 tint={prominent ? tint : undefined}
58 dim={clear && dim && !prominent}
59 interactive
60 className={["fd-gbtn", `fd-gbtn--${size}`, iconOnly ? "fd-gbtn--icon" : "", prominent ? "fd-gbtn--prominent" : "", className ?? ""].filter(Boolean).join(" ")}
61 style={{ "--fd-gbtn-h": `${h}px`, ...style } as CSSProperties}
62 {...rest}
63 >
64 <style>{css}</style>
65 {icon ? (
66 <span className="fd-gbtn-icon" aria-hidden="true">
67 {icon}
68 </span>
69 ) : null}
70 {!iconOnly && children != null ? <span className="fd-gbtn-label">{children}</span> : null}
71 </LiquidGlass>
72 );
73});
74
75const css = `
76.fd-gbtn{display:inline-flex;align-items:center;justify-content:center;gap:8px;flex:none;height:var(--fd-gbtn-h);min-width:var(--fd-gbtn-h);margin:0;padding:0 calc(var(--fd-gbtn-h) * 0.42);vertical-align:middle;white-space:nowrap;user-select:none;-webkit-user-select:none;
77 font:600 15px/1 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:-0.01em;text-align:center}
78.fd-gbtn--s{font-size:13px;gap:6px}
79.fd-gbtn--l{font-size:17px}
80.fd-gbtn--xl{font-size:20px;gap:10px;font-weight:600}
81.fd-gbtn--icon{width:var(--fd-gbtn-h);padding:0}
82.fd-gbtn-icon{display:inline-flex;align-items:center;justify-content:center;margin:0;padding:0;color:inherit}
83.fd-gbtn-icon svg{display:block;width:calc(var(--fd-gbtn-h) * 0.46);height:calc(var(--fd-gbtn-h) * 0.46)}
84.fd-gbtn:not(.fd-gbtn--icon) .fd-gbtn-icon svg{width:calc(var(--fd-gbtn-h) * 0.42);height:calc(var(--fd-gbtn-h) * 0.42);margin-left:-2px}
85.fd-gbtn-label{display:block;margin:0;padding:0;color:inherit;font:inherit;letter-spacing:inherit}
86.fd-gbtn:disabled{opacity:0.42;cursor:default}
87.fd-gbtn:disabled .fd-lg-glow{display:none}
88`;
89
90/* ───────────────────────── demo: hotel photo viewer ───────────────────────── */
91
92const I = {
93 back: <path d="M14.5 4.5 7 12l7.5 7.5" />,
94 share: (
95 <>
96 <path d="M12 3.5v11" />
97 <path d="m8 7 4-3.5L16 7" />
98 <path d="M8.5 10.5H7a1.5 1.5 0 0 0-1.5 1.5v7A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 17 10.5h-1.5" />
99 </>
100 ),
101 heart: <path d="M12 20s-7.5-4.4-7.5-10.1A4.4 4.4 0 0 1 12 7.3a4.4 4.4 0 0 1 7.5 2.6C19.5 15.6 12 20 12 20Z" />,
102 grid: (
103 <>
104 <rect x="4" y="4" width="6.5" height="6.5" rx="1.6" />
105 <rect x="13.5" y="4" width="6.5" height="6.5" rx="1.6" />
106 <rect x="4" y="13.5" width="6.5" height="6.5" rx="1.6" />
107 <rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.6" />
108 </>
109 ),
110 calendar: (
111 <>
112 <rect x="4" y="5.5" width="16" height="14.5" rx="3" />
113 <path d="M4 10h16M8.5 3.5v4M15.5 3.5v4" />
114 </>
115 ),
116 play: <path d="M8 5.8v12.4a.8.8 0 0 0 1.2.7l10-6.2a.8.8 0 0 0 0-1.4l-10-6.2A.8.8 0 0 0 8 5.8Z" fill="currentColor" stroke="none" />,
117 plus: <path d="M12 5v14M5 12h14" />,
118 more: (
119 <>
120 <circle cx="6" cy="12" r="1.6" fill="currentColor" stroke="none" />
121 <circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none" />
122 <circle cx="18" cy="12" r="1.6" fill="currentColor" stroke="none" />
123 </>
124 ),
125};
126const Icon = ({ d }: { d: ReactNode }) => (
127 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
128 {d}
129 </svg>
130);
131
132/** Deterministic whitewashed village along the cliff ridge (SSR-safe, no Math.random). */
133const VILLAGE = Array.from({ length: 34 }, (_, i) => {
134 const t = i / 33;
135 const ridge = 62 - Math.pow(t, 1.6) * 52; // cliff top falls away to the sea
136 const jitter = ((i * 37) % 11) / 11;
137 const row = i % 3;
138 const r1 = (n: number) => Math.round(n * 10) / 10;
139 return {
140 x: r1(t * 74 + jitter * 2),
141 y: r1(Math.max(4, ridge - row * 9 - jitter * 6)),
142 w: r1(4.2 + ((i * 13) % 5) * 0.7),
143 h: r1(6 + ((i * 7) % 4) * 1.6),
144 dome: i === 9 || i === 21,
145 };
146});
147
148export function Demo() {
149 const [saved, setSaved] = useState(false);
150 const [photo, setPhoto] = useState(0);
151 const photos = ["Infinity pool at dusk", "Cliffside suite", "Breakfast terrace"];
152 return (
153 <div className="fd-gbd">
154 <style>{demoCss}</style>
155 <GlassScope>
156 <div className={`fd-gbd-photo fd-gbd-photo--${photo}`} role="img" aria-label={`Hotel Kalyra, Santorini — ${photos[photo]}`}>
157 <div className="fd-gbd-sun" />
158 <div className="fd-gbd-sea" />
159 <div className="fd-gbd-glitter" />
160 <div className="fd-gbd-cliff" />
161 <div className="fd-gbd-village">
162 {VILLAGE.map((h, i) => (
163 <span
164 key={i}
165 className={`fd-gbd-house${h.dome ? " is-dome" : ""}`}
166 style={{ left: `${h.x}%`, bottom: `${h.y}%`, width: `${h.w}%`, height: `${h.h}%`, zIndex: Math.round(100 - h.y) } as CSSProperties}
167 />
168 ))}
169 </div>
170 <div className="fd-gbd-vignette" />
171 </div>
172
173 <header className="fd-gbd-top">
174 <GlassButton glass="clear" size="l" iconOnly aria-label="Back to search results" icon={<Icon d={I.back} />} />
175 <div className="fd-gbd-top-right">
176 <GlassButton glass="clear" size="l" iconOnly aria-label="Share" icon={<Icon d={I.share} />} />
177 <GlassButton
178 glass={saved ? "clearProminent" : "clear"}
179 tint="#FF375F"
180 size="l"
181 iconOnly
182 aria-label={saved ? "Remove from saved" : "Save to wishlist"}
183 aria-pressed={saved}
184 onClick={() => setSaved((s) => !s)}
185 icon={
186 <svg viewBox="0 0 24 24" fill={saved ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2" strokeLinejoin="round">
187 {I.heart}
188 </svg>
189 }
190 />
191 </div>
192 </header>
193
194 <div className="fd-gbd-caption">
195 <p className="fd-gbd-eyebrow">Oia, Santorini · ★ 4.92 (1,284)</p>
196 <h1 className="fd-gbd-title">Hotel Kalyra</h1>
197 <p className="fd-gbd-meta">Cliffside suite with private plunge pool · 12 – 17 Jun</p>
198 <div className="fd-gbd-dots" role="tablist" aria-label="Photos">
199 {photos.map((p, i) => (
200 <button key={p} type="button" role="tab" aria-selected={i === photo} aria-label={p} className="fd-gbd-dot" onClick={() => setPhoto(i)} />
201 ))}
202 </div>
203 </div>
204
205 <div className="fd-gbd-actions">
206 <GlassButton glass="clear" size="l" icon={<Icon d={I.grid} />}>
207 All 48 photos
208 </GlassButton>
209 <GlassButton glass="clear" size="l" iconOnly aria-label="Play video tour" icon={<Icon d={I.play} />} />
210 <GlassButton glass="clearProminent" size="xl" tint="#0A84FF" icon={<Icon d={I.calendar} />}>
211 Reserve · €612/night
212 </GlassButton>
213 </div>
214
215 <aside className="fd-gbd-sheet" aria-label="Button styles">
216 <p className="fd-gbd-sheet-h">Styles &amp; sizes</p>
217 <div className="fd-gbd-row">
218 <GlassButton glass="glass" size="s">Glass S</GlassButton>
219 <GlassButton glass="glass" size="m">Glass M</GlassButton>
220 <GlassButton glass="glassProminent" size="m">Prominent</GlassButton>
221 <GlassButton glass="glass" size="m" iconOnly aria-label="Add guest" icon={<Icon d={I.plus} />} />
222 <GlassButton glass="glass" size="m" iconOnly aria-label="More options" icon={<Icon d={I.more} />} />
223 <GlassButton glass="glass" size="m" disabled>Sold out</GlassButton>
224 </div>
225 </aside>
226 </GlassScope>
227 </div>
228 );
229}
230
231const F = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;
232const D = `"SF Pro Display",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Roboto,sans-serif`;
233
234const demoCss = `
235.fd-gbd,.fd-gbd *,.fd-gbd *::before,.fd-gbd *::after{box-sizing:border-box}
236.fd-gbd{position:relative;height:100vh;min-height:640px;width:100%;overflow:hidden;margin:0;padding:0;background:#0B1B2E;color:#fff;font:400 15px/1.4 ${F};letter-spacing:normal}
237.fd-gbd-photo{position:absolute;inset:0;display:block;overflow:hidden;transition:filter var(--fd-dur-4,700ms) cubic-bezier(0.22,1,0.36,1)}
238.fd-gbd-photo > div{position:absolute;display:block;margin:0;padding:0}
239.fd-gbd-photo{background:linear-gradient(180deg,#1E2A5A 0%,#6C4B8C 22%,#E57A6E 42%,#FFC48A 54%,#FFE3B0 58%)}
240.fd-gbd-photo--1{filter:hue-rotate(-18deg) saturate(1.15)}
241.fd-gbd-photo--2{filter:hue-rotate(24deg) brightness(1.12) saturate(1.1)}
242.fd-gbd-sun{left:62%;top:38%;width:96px;height:96px;margin:-48px 0 0 -48px !important;border-radius:50%;background:radial-gradient(circle,#FFF8E4 0 30%,rgba(255,226,170,0.8) 33%,rgba(255,190,130,0.35) 52%,rgba(255,160,110,0) 72%);transform:scale(2.6)}
243.fd-gbd-sea{left:0;right:0;top:55%;bottom:0;background:
244 repeating-linear-gradient(180deg,rgba(255,226,190,0) 0 7px,rgba(255,226,190,0.22) 7px 8px,rgba(255,226,190,0) 8px 13px),
245 linear-gradient(180deg,#F0B08A 0%,#9D6E8E 6%,#3B4F86 22%,#1A3463 55%,#0A1A36 100%)}
246.fd-gbd-glitter{left:62%;top:55%;bottom:0;width:220px;margin-left:-110px !important;background:
247 repeating-linear-gradient(180deg,rgba(255,238,200,0) 0 5px,rgba(255,238,200,0.85) 5px 6.5px,rgba(255,238,200,0) 6.5px 11px);
248 -webkit-mask:radial-gradient(ellipse 50% 100% at 50% 0%,#000 0 40%,transparent 100%);mask:radial-gradient(ellipse 50% 100% at 50% 0%,#000 0 40%,transparent 100%)}
249.fd-gbd-cliff{left:-10%;bottom:-6%;width:74%;height:60%;border-radius:0 85% 0 0 / 0 100% 0 0;background:
250 radial-gradient(ellipse 60% 40% at 30% 30%,rgba(255,170,120,0.35),transparent 70%),
251 repeating-linear-gradient(160deg,rgba(0,0,0,0) 0 18px,rgba(0,0,0,0.12) 18px 20px),
252 linear-gradient(165deg,#9A5F4A 0%,#6B3A2C 40%,#2E1712 100%)}
253.fd-gbd-village{left:0;bottom:14%;width:70%;height:50%;filter:blur(0.4px)}
254.fd-gbd-house{position:absolute;display:block;margin:0;padding:0;border-radius:2px 2px 0 0;
255 background:linear-gradient(90deg,#FFF8EE 0 62%,#E9C9B4 62% 100%);
256 box-shadow:inset 0 -3px 0 rgba(120,60,40,0.18),0 2px 6px rgba(60,20,10,0.25)}
257.fd-gbd-house::after{content:"";position:absolute;left:24%;top:38%;width:18%;height:26%;margin:0;padding:0;border-radius:40% 40% 0 0;background:#2B3E66}
258.fd-gbd-house.is-dome{border-radius:50% 50% 0 0;background:radial-gradient(circle at 50% 100%,#2453C8 0 70%,#1A3C93 100%)}
259.fd-gbd-house.is-dome::after{display:none}
260.fd-gbd-vignette{left:0;right:0;bottom:0;height:60%;background:linear-gradient(180deg,rgba(6,10,24,0) 0%,rgba(6,10,24,0.5) 45%,rgba(6,10,24,0.82) 100%)}
261.fd-gbd-top{position:absolute;left:0;right:0;top:0;display:flex;justify-content:space-between;align-items:center;margin:0;padding:20px 24px;z-index:2}
262.fd-gbd-top-right{display:flex;gap:12px}
263.fd-gbd-caption{position:absolute;left:32px;bottom:120px;max-width:520px;margin:0;padding:0;z-index:1;text-shadow:0 2px 18px rgba(0,0,0,0.35)}
264.fd-gbd-eyebrow{margin:0 0 8px;font:600 13px/1.3 ${F};letter-spacing:0.02em;color:rgba(255,255,255,0.9)}
265.fd-gbd-title{margin:0 0 8px;font:700 clamp(2.75rem,6vw,5.5rem)/0.95 ${D};letter-spacing:-0.04em;color:#fff}
266.fd-gbd-meta{margin:0 0 18px;font:500 17px/1.35 ${F};letter-spacing:-0.01em;color:rgba(255,255,255,0.92)}
267.fd-gbd-dots{display:flex;gap:6px;margin:0;padding:0}
268.fd-gbd-dot{display:block;width:7px;height:7px;margin:0;padding:0;border:0;border-radius:99px;background:rgba(255,255,255,0.5);cursor:pointer;-webkit-appearance:none;appearance:none;transition:width var(--fd-dur-3,420ms) cubic-bezier(0.34,1.56,0.64,1),background-color 240ms}
269.fd-gbd-dot[aria-selected="true"]{width:22px;background:#fff}
270.fd-gbd-dot:focus-visible{outline:2px solid rgba(10,132,255,0.7);outline-offset:3px}
271.fd-gbd-actions{position:absolute;left:24px;right:24px;bottom:28px;display:flex;align-items:center;gap:12px;margin:0;padding:0;z-index:2}
272.fd-gbd-actions > :last-child{margin-left:auto}
273.fd-gbd-sheet{position:absolute;right:24px;top:96px;margin:0;padding:16px 18px 18px;border-radius:24px;background:rgba(247,247,249,0.94);color:#0B0B0F;box-shadow:0 20px 50px -20px rgba(0,0,0,0.5);z-index:2;max-width:calc(100% - 48px)}
274.fd-gbd-sheet-h{margin:0 0 12px;font:600 13px/1.2 ${F};letter-spacing:0.02em;text-transform:uppercase;color:#6B6B76}
275.fd-gbd-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0;padding:14px;border-radius:12px;background:
276 repeating-linear-gradient(90deg,rgba(11,11,15,0.55) 0 1.5px,transparent 1.5px 11px),
277 linear-gradient(120deg,#FF8A65,#FFCE78 40%,#78C8FF)}
278@media (max-width:820px){
279 .fd-gbd-top{padding:14px 16px}
280 .fd-gbd-top-right{gap:10px}
281 .fd-gbd-caption{left:20px;right:20px;bottom:104px}
282 .fd-gbd-meta{font-size:15px}
283 .fd-gbd-actions{left:16px;right:16px;bottom:20px;gap:10px}
284 .fd-gbd-actions > :first-child{display:none}
285 .fd-gbd-actions > :last-child{flex:1}
286 .fd-gbd-sheet{display:none}
287 .fd-gbd-sun{left:72%;top:34%}
288 .fd-gbd-glitter{left:72%}
289 .fd-gbd-cliff{width:96%;height:52%}
290 .fd-gbd-village{width:92%;height:50%}
291}
292@media (prefers-reduced-motion: reduce){.fd-gbd-dot,.fd-gbd-photo{transition-duration:120ms}}
293`;

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 Tab Bar

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

Glass Toggle

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