Anatomy of the Liquid Glass Card
How a rim-banded displacement map, a chromatic triple-displace filter, and a pointer sheen turn backdrop-filter into living glass — with a clean frost fallback when url() is unsupported.
LiquidGlassCard is not a PNG overlay or a static blur. It samples the
live DOM behind the panel through backdrop-filter, bends that sample
with an SVG displacement map sized to the card, and fringes color at the rim
by running the bend at three scales. Children stay crisp above the glass in
relative z-raised.
Frost, sheen, rim — three layers
Three absolute layers share the same rounded box:
| Layer | Job |
|---|---|
| Frost | backdrop-filter: blur + saturate (+ url(#filter) when supported) |
| Sheen | mix-blend-screen radial that tracks the pointer |
| Rim | static inset highlight / shadow for a wet edge |
backdropFilter: active
? `blur(${blur}px) saturate(${saturation}) url(#${filterId})`
: `blur(${blur}px) saturate(${saturation})`;
// WebkitBackdropFilter stays blur+saturate only — Safari can't take url()Defaults: radius={28}, blur={2}, strength={60}, dispersion={0.15},
saturation={1.6}, sheen={0.5}.
Same rounded box, three jobs. The dashed mid on the map is the neutral band — no bend under your content. (Real maps encode X in R and Y in G; diagrams stay grayscale.)
- Frost
- backdrop-filter blur + optional url(#filter)
- Sheen
- pointer-tracked radial · mix-blend-screen
- Rim map
- band=0.3 — neutral mid, bend only at edges
The map bends the rim, not the center
buildDisplacementMap(w, h, band) paints a data-URL SVG. Under the hood, red
encodes horizontal shift and green vertical; mid-gray 128
(#800000 / #008000) means no shift. That channel packing is an SVG
filter detail — Learn diagrams stay grayscale (dashed mid = no bend) so the
band shape reads without borrowing map hues. With band={0.3} the mid stays
neutral so only the perimeter bends — the flat-panel / convex-edge look:
const lo = (0.5 - band / 2).toFixed(3); // 0.350 at band 0.3
const hi = (0.5 + band / 2).toFixed(3); // 0.650
// linearGradient stops: edge → lo (neutral) → hi (neutral) → opposite edgeA ResizeObserver rebuilds the map whenever the card's content box changes,
so the rim stays registered to real geometry after fonts load or content
reflows.
const map = buildDisplacementMap(size.width, size.height, 0.3);Chromatic triple displace
RefractionFilter runs feDisplacementMap three times against the same map,
isolates R / G / B with feColorMatrix, then recombines with
feBlend mode="screen". Dispersion widens the fringe where the bend is
strongest. The Learn diagram shows that as three grayscale offsets — the
mechanism is the scale difference, not a rainbow UI.
| Channel | Scale |
|---|---|
| Red | strength * (1 + dispersion) |
| Green | strength |
| Blue | strength * (1 - dispersion) |
<feImage href={map} result="map" preserveAspectRatio="none" />
<feDisplacementMap scale={strength * (1 + dispersion)} result="dispR" />
<feColorMatrix in="dispR" values={RED_CHANNEL} result="red" />
{/* green @ strength, blue @ strength * (1 - dispersion) */}
<feBlend in="red" in2="green" mode="screen" result="rg" />
<feBlend in="rg" in2="blue" mode="screen" />Three grayscale ghosts at different offsets stand in for the R/G/B displaces — hue is not required to teach the fringe. Then feBlend mode="screen".
- R
- strength × (1 + d)
- G
- strength
- B
- strength × (1 − d)
Push strength / dispersion up for a heavier prismatic edge; drop them for
subtle frost.
Pointer sheen
On pointermove, the card writes CSS variables as percentages of its own
box (not pixels):
node.style.setProperty(
"--lg-x",
`${((e.clientX - rect.left) / rect.width) * 100}%`,
);
node.style.setProperty(
"--lg-y",
`${((e.clientY - rect.top) / rect.height) * 100}%`,
);The sheen layer paints
radial-gradient(circle at var(--lg-x) var(--lg-y), …) and toggles opacity
on enter / leave with transition-opacity duration-200.
Under prefers-reduced-motion, pointer updates are skipped and the sheen
layer is motion-reduce:hidden — frost stays, specular dies.
- CSS vars
- --lg-x / --lg-y as %
- Fade
- transition-opacity duration-200
Chrome-only url(), clean fallback
Live-DOM refraction needs backdrop-filter: url(#id) — Chrome and Edge
today. useRefractionSupport probes once:
CSS.supports("backdrop-filter", "url(#a)")Without support, the SVG filter is omitted and frost is blur + saturate only.
Safari and Firefox still get a believable glass panel; they never render as a
broken transparent box. Decorative layers and the filter SVG are
aria-hidden.
The result
Move across the panel. Watch the rim fringe and the specular follow.
One map, three displaces, one CSS sheen — and a frost fallback when the GPU path is unavailable.