CSS Liquid Glass Panel
A frosted glass panel with an SVG displacement distortion for Chromium and a blur fallback for Firefox and Safari, plus a cross-browser copy-layer variant.
- Level
- Advanced
- Published
- Updated
- Reading time
- 5 min
Try it
2 live demos
The stripes bend as they pass behind it.
Glass that bends what is behind it
Other ways to build it
This panel filters its own copy of the stripes.
A version that bends in Firefox, Safari, and Chromium
See the codeBuild it
4 snippets to copy
CSS Liquid Glass Panel
HTML
<svg width="0" height="0" aria-hidden="true">
<filter id="lg-bend" x="0" y="0" width="100%" height="100%"
color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency=".012 .018"
numOctaves="2" seed="4"/>
<feGaussianBlur stdDeviation="2" result="soft"/>
<feDisplacementMap in="SourceGraphic" in2="soft" scale="46"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<div class="lg-scene">
<div class="lg-panel">
<strong>Liquid glass</strong>
<p>The stripes bend as they pass behind it.</p>
</div>
</div>
CSS
.lg-scene {
position: relative;
overflow: hidden;
height: 220px;
display: grid;
place-items: center;
}
/* The stripes drift right to left. The layer is one period
wider than the scene and moves exactly one period
(90px / sin 115deg = 99.3px), so the loop has no seam. */
.lg-scene::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: calc(100% + 100px);
background: repeating-linear-gradient(115deg,
#ff9040 0 18px, #ffd444 18px 36px, #57d9a3 36px 54px,
#38bdf8 54px 72px, #c084fc 72px 90px);
animation: lg-drift 40s linear infinite;
}
@keyframes lg-drift {
to { transform: translateX(-99.3px); }
}
@media (prefers-reduced-motion: reduce) {
.lg-scene::before { animation: none; }
}
/* Frosted glass as the base. Firefox and Safari accept url()
here and then paint nothing, so it cannot go in this rule. */
.lg-panel {
width: min(80%, 290px);
padding: 1.15rem 1.3rem;
border: 1px solid rgba(255, 255, 255, .6);
border-radius: 22px;
color: #101012;
background: rgba(255, 255, 255, .16);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7),
0 10px 28px rgba(0, 0, 0, .28);
-webkit-backdrop-filter: blur(9px) saturate(1.3);
backdrop-filter: blur(9px) saturate(1.3);
}
/* Chromium is the one engine that runs an SVG filter
inside backdrop-filter. */
@supports (-webkit-app-region: inherit) {
.lg-panel {
backdrop-filter: url(#lg-bend) blur(1px) saturate(1.4);
}
}
A version that bends in Firefox, Safari, and Chromium
The main panel's backdrop-filter method works only in Chromium. This version sidesteps that by copying the striped background into a ::before and applying filter: url(#lg-bend) to it. The trade-off is keeping the copy in sync with the scene behind it.
HTML
<div class="lg-scene">
<div class="lg-panel lg-copy">
<strong>Bent in Chromium, Firefox and WebKit</strong>
</div>
</div>
CSS
/* adds to the .lg-scene and .lg-panel rules and the SVG above */
.lg-copy {
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, .05);
backdrop-filter: none;
}
.lg-copy strong {
position: relative;
}
/* filter, unlike backdrop-filter, runs url() in all three
engines, so the panel carries a copy of the scene and bends that */
.lg-copy::before {
content: "";
position: absolute;
inset: -30px;
background: repeating-linear-gradient(115deg,
#ff9040 0 18px, #ffd444 18px 36px, #57d9a3 36px 54px,
#38bdf8 54px 72px, #c084fc 72px 90px)
0 0 / calc(100% + 100px) 100% no-repeat;
filter: url(#lg-bend);
animation: lg-drift-copy 40s linear infinite;
}
/* moves the background, not the layer, so the bends stay
put and the stripes pass through them */
@keyframes lg-drift-copy {
to { background-position: -99.3px 0; }
}
@media (prefers-reduced-motion: reduce) {
.lg-copy::before { animation: none; }
}
Understand it
How it works and 5 properties
A frosted glass effect comes from backdrop-filter: blur(), which all major browsers support. Getting the content behind the panel to bend rather than blur requires a displacement filter, which maps backdrop pixels through a noise field and shifts them.
The displacement path has an engine limit. An SVG filter referenced with url() inside backdrop-filter works only in Chromium, so the CSS uses an @supports test to turn that on there and keep plain blur elsewhere. The variant section below solves it differently: it copies the scene into a ::before inside the panel and applies filter to that copy.
How it works
The scene is a repeating diagonal stripe, which makes displacement easy to read because straight lines kink where the noise shifts them. The panel gets backdrop-filter: blur(9px) saturate(1.3) as the base. Inside @supports (-webkit-app-region: inherit), Chromium switches to backdrop-filter: url(#lg-bend) blur(1px) saturate(1.4), adding the SVG filter to the chain. The filter runs three stages: feTurbulence generates noise, feGaussianBlur softens it, and feDisplacementMap uses the result to shift backdrop pixels by the amount set with scale.
The SVG filter chain runs in sequence. feTurbulence with type="fractalNoise" generates a smooth noise field at the frequency set by baseFrequency=".012 .018". Keeping that value low produces slow, wave-like variation rather than pixel-level noise. feGaussianBlur with stdDeviation="2" softens the turbulence output before it becomes the displacement map, which smooths the distortion edges.
feDisplacementMap takes two inputs: SourceGraphic, the content being filtered, and soft, the blurred noise result. The xChannelSelector="R" and yChannelSelector="G" attributes pick the noise channels that drive the horizontal and vertical shifts. scale="46" sets the displacement strength. Higher values produce more bending; lower values approach clear glass.
The @supports (-webkit-app-region: inherit) condition targets Chromium by checking for a property only it recognizes. That property does nothing visual; its presence in the feature query routes the SVG filter path to Chromium. Firefox and Safari skip the block and use the blur() fallback defined outside it.
The copy-layer variant reverses the strategy. Instead of filtering the backdrop, it paints a copy of the background inside the panel as ::before and runs filter: url(#lg-bend) on that. filter takes url() in Firefox, Safari, and Chromium. The pseudo-element uses inset: -30px to extend past the panel edges, giving displaced pixels room to shift without a gap at the border. The panel's own overflow: hidden clips the excess.
Accessibility notes
The panel text is standard HTML. The decorative stripe scene is a CSS background-image and is absent from the accessibility tree.
The <svg> element uses aria-hidden="true" to keep it out of the accessibility tree. Browsers skip inline SVGs in the tab order by default.
Mistakes worth avoiding
- Putting the SVG
url()rule outside an@supportstest. Firefox and Safari acceptbackdrop-filter: url(#lg-bend)but render nothing from it. Without the guard, that rule replaces the workingblur(9px)fallback in those browsers, and the panel loses its frosted glass. - Leaving the filter
<svg>withoutaria-hidden="true". Without it the invisible element may appear in the accessibility tree. - Using the copy-layer variant without keeping the
::beforescene in sync with the real background. - Adapting the color palette without rechecking text contrast. This demo's dark text on the displaced stripe colors measures 7:1 or better. A different palette may not.
- Running the displacement filter on a large region. SVG displacement in
backdrop-filtercan be expensive in Chromium's compositing pipeline on wide panels.
Ship it
3 features in 4 browsers, 5 questions
Browser support
| Feature | ||||
|---|---|---|---|---|
backdrop-filter | 76 | 103 | 9 | 17 |
filter | 18 | 3.6 | 6 | 13 |
SVG filters | 5 | 3 | 6 | 12 |
The frosted blur fallback, backdrop-filter: blur(), covers Firefox 103, Safari 9, and Chromium 76. The SVG displacement path runs only where the @supports condition matches. filter with a url() reference works in all three.
What you can build with it
- Floating UI panel over a colorful background. A modal or control that lets the underlying scene show through without fully obscuring it.
- Controls over album art or a photo. A panel for playback or metadata that references the image behind it visually while staying readable.
- Dashboard widget over a chart or map. An overlay that contains its own content while the data underneath stays visible.
- Notification tray or toast. A message container that sits above a busy page without a hard opaque edge.
- Navigation drawer as an overlay. A panel that slides over content rather than pushing it, where the page remaining visible reduces disorientation.
Frequently asked questions
Why does the SVG displacement filter only work in Chromium?
backdrop-filter: url() value but leave the backdrop unchanged. The @supports guard keeps that rule in Chromium only, so the others use the blur() fallback.What does the @supports condition check?
-webkit-app-region exists only in Chromium's engine.How does the copy-layer variant work in Firefox and Safari?
filter to a ::before copy of the background rather than the backdrop. Because the filter runs on the element's own content, the engine restriction does not apply.Why does the copy-layer panel use inset: -30px?
overflow: hidden clips everything back to the border.How do I increase the amount of bending?
scale attribute on feDisplacementMap. Raise stdDeviation on feGaussianBlur proportionally to keep the distortion edges smooth.