Liquid glass for the web

Glass refracts. Light passing through it bends, hardest where the surface curves down at the rim, so the world behind a pane warps and brightens along its edge. That bend is the whole tell. Take it away and you have a frosted panel, which is what nearly every “liquid glass” on the web actually is: a blur with a bright border. Blur isn’t glass.

We wanted the bend itself, computed from the optics rather than faked with a gradient, and working in every browser, Safari and Firefox included, not just Chromium. This walks through both halves: the physics that shapes the refraction, and the one move that gets it running everywhere. The dropdown pinned above is the result; it bends the photo looping behind it, so hover a nav item to open it.

The bend is computed, not painted

The whole effect is one value per pixel: how far to shove the backdrop sideways there. Paint that as a soft radial gradient and you get something glassy enough, which is where most demos stop. We compute it from the surface instead, so the distortion is the one a real lens would make.

We model the lens as a physical object: a convex squircle dome, flat across the centre and curving down through a thin rim band, the same superellipse Apple rounds its corners with. For each pixel we take that dome’s slope, refract a straight-down view ray through it with Snell’s law at an index of 1.5 (real glass), and the sideways throw of the bent ray becomes the displacement, aimed along the surface normal we read off a signed-distance field of the rounded rectangle. The geometry does the rest. The slope is zero in the flat centre and steepens toward the edge, so all the bending concentrates at the rim while the centre stays clear, which is exactly how a thick piece of real glass behaves. A blue channel carries the specular height that becomes the bright rim light. No invented curve, just optics.

// The surface is a squircle dome. x = depth in from the rim
// (0 = edge, 1 = flat centre); slope = the surface tilt.
const slope  = (1 - x) ** 3 / (1 - (1 - x) ** 4) ** 0.75
const thetaI = Math.atan(slope)
const thetaT = Math.asin(Math.sin(thetaI) / 1.5) // Snell's law
const bend   = Math.sin(thetaI - thetaT) // 0 centre, max at rim

// aim the bend along the rounded-rect SDF normal (nx, ny), then
// write it to the map's R/G channels (128 = leave it put)
map.r = 128 + nx * bend * gain
map.g = 128 + ny * bend * gain

That map drives one SVG filter, feDisplacementMap: for each pixel it looks up the offset stored in the map and moves the content there by exactly that much. We wrote those offsets straight from the refraction math, so what comes out is the bend real glass makes, not a gradient tuned to look close.

Refract a copy, not the backdrop

The optics were the easy half. The hard half is a pure web problem: getting real backdrop pixels into that filter in every browser. The obvious route is a trap. backdrop-filter will run an SVG displacement filter, but only in Chromium. Safari and Firefox accept the property, silently drop the SVG part, and leave you a flat blur. That one gap is why most web “liquid glass” is glass in Chrome and frosting everywhere else.

The way through is to stop filtering the live backdrop and filter a copy of it. feDisplacementMap runs through the ordinary CSS filter property everywhere; only backdrop-filter is Chromium-only. So we render the backdrop a second time, counter-position that copy 1:1 under the lens, and bend the copy. The real interface beneath is never filtered; it stays fully interactive while the bent copy lies over it as nothing but light. We took this move, filtering a copy rather than the backdrop, from Aave’s write-up, Building Glass for the Web, which is where the credit belongs. The optics that shape the map are ours; this is the architecture that lets them run anywhere without a WebGL pipeline or a screenshot.

<GlassScene content={<Backdrop />}>
  <nav>{triggers}</nav>

  {open && (
    <GlassLens x={x} y={y} width={W} height={h}>
      {menuItems}
    </GlassLens>
  )}
</GlassScene>

GlassScene renders the backdrop once and shares it; GlassLens drops a counter-positioned copy into the lens box and bends it. Because the copy tracks the backdrop frame for frame, the glass refracts live, moving content. Scroll, or let the photo loop, and the lens keeps bending whatever sits behind it.

Worth saying why a copy and not one of the flashier routes. A WebGL screenshot gives real optics but freezes the page into a stale, unselectable texture. Firefox’s element() renders a live element as an image, exactly what we want, and exists in no other browser. Houdini’s paint worklets are barred from reading existing pixels. Pure CSS never touches the backdrop at all, so it’s a convincing photo of glass, not glass. A live copy through a real filter is the only route that’s both honest and universal.

The details that bite

That’s the idea. Shipping it across browsers is a list of small, unobvious things, some ours and some flagged in Aave’s write-up.

The displacement map has to reach the filter as a blob: URL. Every guide tells you to inline it as a data: URI; real WebKit silently refuses to load a data: URI inside feImage, the map never arrives, and the glass collapses to flat frost. Blobs load everywhere. Filters also run in linearRGB by default, which quietly changes how far a given grey value displaces, so we force sRGB and the map means what it says.

Two more Safari concessions, both called out in Aave’s write-up: it caches a filter’s output by its id, so a lens that animates needs a fresh id on every rebuild or it freezes on the first frame; and it caps how large a source graphic a filter will process, so the refraction copy is clipped to the lens box before it rasterises. Hand it a full-viewport scene and the filter silently produces nothing.

The browsers also get different chains. Chromium runs SVG filters on the GPU, so the full effect (three displacement passes at slightly different scales for a chromatic fringe at the rim, plus the specular pass) is essentially free. Safari runs them in software, and the backdrop here pans without stopping, so the filter re-rasterises every single frame; a second displacement pass for the fringe is more than that budget allows without dropping frames. So Safari runs one pass: the same dome refraction and the same rim highlight, minus the faint colour split. Same material, scaled to what each browser can afford, and it never falls back to a flat blur.

One lens, alive

There’s a single lens, not one per menu. Move across the nav and it travels to the next item and resizes to that dropdown’s shape, one pane of glass changing size and place as you go. The motion is spring-driven and interruptible, so a fast sweep across the triggers pulls it onto whichever item you land on, and the menu rows rise in a quick stagger once it arrives. What you read is continuity: glass held as one moving piece.

Accessible by default

Refraction is motion and translucency, and not everyone wants either. With prefers-reduced-motion the lens stops animating; it cuts to its new position instead of springing. With prefers-reduced-transparency, the system-level “I can’t read text on glass” switch, the effect drops the refraction entirely for an opaque panel, so labels sit on a solid surface at full contrast. The glass is an enhancement; the interface underneath it works without it.

The engine

Two components and one filter primitive are the whole engine. GlassScene shares the backdrop; GlassLens bends a copy of it through the optics above. The refraction was never the hard part; getting honest backdrop pixels into a real filter, in every browser, was. Solve that once and the glass is only a few hundred lines. And what you get isn’t a blur doing an impression of glass. It’s real refraction, and it works in Safari and Firefox, not just Chrome.