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 refracting glass, with a frost fallback when url() is unsupported.
↓Scroll to step through it
Frost, sheen, rim: three layers
LiquidGlassCard 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 unblurred above the glass in
relative z-raised.
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 |
Defaults: radius={28}, blur={2}, strength={60}, dispersion={0.15},
saturation={1.6}, sheen={0.5}.
The map bends the rim, not the center
buildDisplacementMap(w, h, band) paints a data-URL SVG. 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 and only
the perimeter bends, giving 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 the card's 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, because
the mechanism is the scale difference between channels.
| Channel | Scale |
|---|---|
| Red | strength * (1 + dispersion) |
| Green | strength |
| Blue | strength * (1 - dispersion) |
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):
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. The frost stays; the specular highlight goes.
Chrome-only url(), clean fallback
Live-DOM refraction needs backdrop-filter: url(#id), which only Chrome and
Edge support 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, and one CSS sheen, with a frost fallback when the GPU path is unavailable.
Same rounded box, three jobs. The dashed mid on the map is the neutral band, with 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
backdropFilter: active? `blur(${blur}px) saturate(${saturation}) url(#${filterId})`: `blur(${blur}px) saturate(${saturation})`;// WebkitBackdropFilter stays blur+saturate only — Safari can't take url()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 refracting glass, with a frost fallback when url() is unsupported.
Same rounded box, three jobs. The dashed mid on the map is the neutral band, with 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
backdropFilter: active? `blur(${blur}px) saturate(${saturation}) url(#${filterId})`: `blur(${blur}px) saturate(${saturation})`;// WebkitBackdropFilter stays blur+saturate only — Safari can't take url()Frost, sheen, rim: three layers
LiquidGlassCard 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 unblurred above the glass in
relative z-raised.
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 |
Defaults: radius={28}, blur={2}, strength={60}, dispersion={0.15},
saturation={1.6}, sheen={0.5}.
The map bends the rim, not the center
buildDisplacementMap(w, h, band) paints a data-URL SVG. 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 and only
the perimeter bends, giving 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 the card's geometry after fonts load or content
reflows.
const map = buildDisplacementMap(size.width, size.height, 0.3);Three grayscale ghosts at different offsets stand in for the R/G/B displaces. Hue isn't needed to show the fringe. Then feBlend mode="screen".
- R
- strength × (1 + d)
- G
- strength
- B
- strength × (1 − d)
<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" />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, because
the mechanism is the scale difference between channels.
| Channel | Scale |
|---|---|
| Red | strength * (1 + dispersion) |
| Green | strength |
| Blue | strength * (1 - dispersion) |
Push strength / dispersion up for a heavier prismatic edge; drop them for
subtle frost.
- CSS vars
- --lg-x / --lg-y as %
- Fade
- transition-opacity duration-200
node.style.setProperty("--lg-x",`${((e.clientX - rect.left) / rect.width) * 100}%`,);node.style.setProperty("--lg-y",`${((e.clientY - rect.top) / rect.height) * 100}%`,);Pointer sheen
On pointermove, the card writes CSS variables as percentages of its own
box (not pixels):
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. The frost stays; the specular highlight goes.
Chrome-only url(), clean fallback
Live-DOM refraction needs backdrop-filter: url(#id), which only Chrome and
Edge support 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, and one CSS sheen, with a frost fallback when the GPU path is unavailable.
Motion Score
transformCompositor transform (translate / scale / rotate)opacityFade / cross-fade