| name | liquid-glass |
| description | Implement real Apple-style "Liquid Glass" (iOS 26 / macOS 26) edge refraction for web UI - navbars, pills, cards, toolbars, modals. Produces actual lens-style bending and chromatic aberration at the rounded edges using a single SVG feDisplacementMap filter and a small generated SDF displacement-map PNG, applied via backdrop-filter - not WebGL, not html2canvas, no per-frame JS. Use this whenever the user asks for "liquid glass", "glassmorphism", Apple-style frosted/glass UI, or says an existing glass effect "is just blur, not real glass", looks like a fish-eye/pinch instead of a magnifying lens, or shows uniform shear/distortion across the whole element. Includes Next.js/React-specific guidance for avoiding hydration mismatches. Works with any framework that can render an inline SVG and apply CSS. |
Liquid Glass
Real edge refraction for glass UI elements, built from three pieces:
- A displacement map PNG (generated by a bundled script) - encodes
"how much and which direction to bend" per pixel, following the
element's actual rounded shape
- A hidden SVG
<filter> using feDisplacementMap (+ optional
chromatic aberration passes) that reads that map
- One CSS rule applying
backdrop-filter: blur() url(#filter-id) ...
Total runtime cost: the PNG (a few KB) plus the filter, which runs as
part of the GPU compositing pass that backdrop-filter: blur() already
performs. No WebGL, no DOM screenshots, no per-frame JavaScript.
If you need the deeper "why" behind any step (debugging unexpected
distortion, tuning the falloff, understanding the chromatic-aberration
trick), read references/theory.md. The steps below are sufficient for a
correct first implementation.
Step 1 - Identify the target element's shape
You need:
- Its border-radius in pixels (or the Tailwind class -> px equivalent,
e.g.
rounded-full on a 64px-tall element = 32px radius)
- Its approximate width and height (or aspect ratio) at the
breakpoint(s) where the glass effect matters most
These don't need to be exact - the map is stretched (preserveAspectRatio="none")
to fill the element - but the radius-to-size ratio should be roughly
right, or the bend will appear in the wrong place (e.g. too far from the
actual rounded corners).
Step 2 - Generate the displacement map
pip install -r scripts/requirements.txt
python3 scripts/generate-displacement-map.py \
--width <approx element width> \
--height <approx element height> \
--radius <border-radius in px> \
--mode sdf \
--output <project>/public/liquid-lens-map.png
- Use
--mode sdf (the default). Never use --mode linear for the actual
effect - it's included only as a "what NOT to do" comparison.
--rim (optional, defaults to --radius) controls how far the bend
extends inward from the edge. Increase for a "thicker" glass look.
- Save the output into the project's static assets directory (e.g.
public/ in Next.js) so it's served as a plain static file.
Regenerate this map if the element's aspect ratio or border-radius
changes meaningfully later.
Step 3 - Add the SVG filter (server-rendered, rendered ONCE)
Copy assets/LiquidGlassFilter.tsx into the project's components
directory, adjusting the mapSrc default to match the path from Step 2.
Render <LiquidGlassFilter /> once, near the root of the app, in a
component that is rendered on the server with fully static output:
- Next.js App Router: in
app/layout.tsx (a server component), inside
<body>, before {children}
- Other React frameworks: anywhere that renders identically on server
and client and isn't re-rendered by client-side state
Do not render it inside a "use client" component that the glass
element itself lives in. If the SVG filter definition is part of a client
component's output, React can produce a server/client markup mismatch on
first paint -> hydration error. Keep the filter definition in static,
server-rendered markup; reference it from CSS by id (url(#liquid-lens))
from anywhere.
If the framework has no server/client rendering split at all (plain SPA,
static site), this isn't a concern - just render the filter once near the
root.
Step 4 - Add the CSS
Copy assets/liquid-glass.css into the project (or merge its rules into
an existing global stylesheet) and import/include it once globally.
Apply the .liquid-glass class to the target element, alongside whatever
shape classes it already has (e.g. Tailwind's rounded-full,
rounded-2xl, a fixed height, etc.):
<nav className="liquid-glass rounded-full h-16 px-6 flex items-center">
...
</nav>
Adjust the --lg-* CSS custom properties (tint, rim brightness, specular
highlights, shadow) to match the project's design system - they're
documented inline in the CSS file. Provide light/dark variants if the
project has a theme system (the bundled CSS includes a
prefers-color-scheme: dark block as a starting point).
Step 5 - Verify the bend direction
Load the page and look at content scrolling/visible behind the glass
element near its rounded edges:
- Correct (magnifying lens): content near the edge appears to bulge
slightly toward the viewer / magnify, with a faint color fringe
(red/blue separation) right at the edge
- Wrong (fish-eye / pinch): content near the edge appears to shrink or
recede, like looking through a peephole
If you see the fish-eye case, the scale value in <LiquidGlassFilter>
needs to be negative (it defaults to -42 - if it's been changed to a
positive number, that's the bug). See references/theory.md for why the
sign matters.
If the bend appears in the wrong location (e.g. middle of an edge instead
of near a corner, or offset from the visible rounded corner), the
displacement map's aspect ratio / radius doesn't match the element -
regenerate it with corrected --width/--height/--radius (Step 2).
Step 6 - Confirm graceful degradation
The CSS includes a plain blur() saturate() declaration before the
url(#liquid-lens) one. Browsers without url()-in-backdrop-filter
support (Safari/Firefox at time of writing) will use the plain-blur
fallback automatically - this is normal, not a bug. Don't add
@supports queries for this; the cascade handles it.
If the project needs to support browsers with no backdrop-filter at
all, the bundled CSS includes an @supports not (...) block providing a
solid near-opaque background instead.
Quick troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|
| No bend at all, just blur | url(#liquid-lens) filter id mismatch, or filter not rendered, or non-Chromium browser | Check the id prop matches the CSS url(#...); confirm <LiquidGlassFilter> is in the rendered HTML; test in Chrome |
| Hydration mismatch error mentioning the SVG/filter | Filter rendered inside a client component | Move <LiquidGlassFilter /> to a server-rendered root layout |
| Fish-eye / pinch / shrinking | Positive scale | Make scale negative |
| Whole element shears uniformly in one direction | Map generated with --mode linear, or a non-SDF map | Regenerate with --mode sdf |
| Bend in wrong place relative to corners | Map aspect ratio / radius doesn't match element | Regenerate with correct --width --height --radius |
| Effect missing in Safari/Firefox | Expected (no url() filter support yet) | Confirm fallback blur() declaration looks acceptable on its own |
Bundled resources
scripts/generate-displacement-map.py - generates the SDF displacement
map PNG (Step 2)
assets/LiquidGlassFilter.tsx - React component for the SVG filter
(Step 3)
assets/liquid-glass.css - the .liquid-glass CSS rule + theming
variables (Step 4)
references/theory.md - deeper explanation of the SDF map, the
feDisplacementMap math, scale sign, chromatic aberration, and the
CSS fallback mechanism