Drag me, or click the backdrop

Use the arrow keys to move the panel, hold Shift for bigger steps.

Frosted glass

Backdrop blur, turbulence-driven displacement, a whisper of grain and a thin rim lit from one side.

How it works

Frosted glass, layer by layer

The glass is a stack of five cheap layers. None of them looks like much on its own; together they read as a physical material.

The stack, back to front

  1. Backdrop. Whatever is rendered behind the panel. The panel never copies it; the browser reads it while compositing.
  2. Backdrop filter. Warps, blurs and color-corrects that backdrop, but only inside the panel's border box (clipped to its border-radius).
  3. Tint. A translucent background color on the panel itself. It also decides the text color (dark on light tints, light on dark ones), and its opacity is what keeps that text readable: the default of 0.35 is the lowest value that keeps body text at 4.5:1 or better over every backdrop on this page.
  4. Grain. A faint noise layer, the same component as on the noise page.
  5. Rim and glow. A one-pixel gradient border plus an inset shadow, which sell the edge of a slab of glass.

1. backdrop-filter: blur, saturate, brighten

backdrop-filter takes the same functions as filter, but applies them to what is behind the element. The functions run left to right, each on the result of the previous one:

blur()
The frost itself. Higher values erase more detail. The blur is clipped to the panel, so the edges stay sharp.
saturate()
Blurring averages neighboring colors, which drifts them towards grey. Pushing saturation back up (around 1.4–1.8) keeps the panel looking lit from behind instead of muddy.
brightness()
A slight lift reads as light passing through the glass.

2. Displacement: feTurbulence → feDisplacementMap

Real frosted or textured glass doesn't just blur, it refracts: the backdrop looks shifted and wavy. backdrop-filter also accepts url(#id), a reference to an SVG filter, and the browser runs the backdrop through it like any image. The filter has two steps:

  1. <feTurbulence type="fractalNoise"> generates a smooth noise image (the map). A low base frequency gives large, gentle waves; a high one gives a pebbled, rippled texture.
  2. <feDisplacementMap> moves every pixel of the backdrop by an amount read from the map: the red channel moves it horizontally, the green channel vertically. A channel value of 0.5 means no movement; 0 and 1 mean half of scale pixels in either direction. In formula form: P′(x, y) = P(x + scale·(R − ½), y + scale·(G − ½)).

The displacement comes first in the chain, so the following blur smooths out the jagged edges that pixel-shifting creates. Pixels near the border that would be fetched from outside the panel come out transparent; the rim hides that seam.

<svg width="0" height="0" aria-hidden="true">
  <filter id="displace" x="0" y="0" width="100%" height="100%"
          color-interpolation-filters="sRGB">
    <feTurbulence type="fractalNoise" baseFrequency="0.008"
                  numOctaves="2" seed="7" result="map" />
    <feDisplacementMap in="SourceGraphic" in2="map" scale="40"
                       xChannelSelector="R" yChannelSelector="G" />
  </filter>
</svg>

3. The rim: a masked gradient border

A plain border can't fade along its length. Instead, a ::after pseudo-element covers the panel, gets padding equal to the rim width and a linear-gradient background. Then two masks are combined with exclude: one covering the whole box and one covering only the content box. What remains visible is just the padding, a gradient-colored ring that follows the border-radius.

The gradient is bright at one end, nearly transparent in the middle and brighter again at the other end: a highlight where the light hits, plus a weaker reflection on the opposite edge. The Light angle slider is the gradient's angle. An inset box-shadow adds the inner glow, a regular one the drop shadow.

.glass {
  position: relative;
  border-radius: 28px;
  background: rgb(255 255 255 / 0.35);
  backdrop-filter: url(#displace) blur(14px) saturate(1.6) brightness(1.05);
}

/* the rim: a gradient that only shows in the padding box */
.glass::after {
  content: '';
  position: absolute;
  inset: 0;
  padding: 1px;
  border-radius: inherit;
  background: linear-gradient(135deg,
    rgb(255 255 255 / 0.7), transparent 35% 65%,
    rgb(255 255 255 / 0.42));
  mask: linear-gradient(#000 0 0) content-box exclude,
        linear-gradient(#000 0 0);
  pointer-events: none;
}

4. Grain

Large blurs over smooth gradients show visible color banding. A very faint noise layer blended with soft-light dithers those bands away and gives the surface a tactile, matte quality. It sits above the tint and below the content, and inherits the panel's rounded corners.

Browser support

Blur, tint, grain and rim work in all current browsers. The displacement renders in Chromium-based browsers only. Firefox (tested in 156) accepts backdrop-filter: url(#…) blur(…) but then renders no backdrop filter at all: no displacement and no blur, just a transparent panel. Safari is untested. Feature detection doesn't help: CSS.supports() only checks the syntax and says yes everywhere. So the component leaves url() out unless it runs in Chromium, and other browsers keep the frost. On this page you can see the failure for yourself with Try it anyway.

Cost: the backdrop filter is re-evaluated whenever anything behind the panel changes, such as the animated backdrop on this page. Large blur radii and strong displacement over big areas are the expensive parts; small panels are cheap.

Back to the demo ↑