Skip to content
WebTricks

Liquid glass CSS — the Apple liquid glass effect with CSS only (plus SVG refraction)

A complete liquid glass CSS tutorial: frosted glass core, light-mode variant, SVG feDisplacementMap refraction, browser support table, accessibility and performance notes.

Apple’s Liquid Glass (introduced across iOS, iPadOS and macOS in 2025) is the current reference for translucent UI. The good news: around 80% of the look is plain, well-supported CSS. The catch: the bending refraction — where the background visibly warps as it passes through the glass — needs an SVG displacement filter that, today, only Chromium can feed into backdrop-filter.

This guide builds the effect in layers so you can stop at the layer your support target allows. Every block is copy-paste ready; the control-driven version lives in the liquid glass card snippet.

What “liquid glass” actually is

Look at a Liquid Glass control up close and you can separate five layers. Each maps to one CSS feature:

LayerWhat it doesCSS
TintA faint white (or black) wash so the pane reads as a material, not a holebackground: rgba(255,255,255,.1)
Blur + saturationFrosts what is behind and pumps its colors so the glass looks thickbackdrop-filter: blur() saturate()
Specular rimA thin bright edge, strongest at the top, where light enters the paneinset box-shadow highlights + a 1px border
SheenA soft diagonal gloss across the surfacea ::after with a linear-gradient and mix-blend-mode: screen
RefractionThe backdrop bends near the edges and ripples slightlybackdrop-filter: url(#filter) with feDisplacementMap

Classic glassmorphism (the 2020 trend) is layers one to three. Liquid glass adds the sheen and refraction, and uses stronger saturation so the material looks lit from inside. If you only need the simpler flat version, the glassmorphism card snippet is enough.

1. The cross-browser core (liquid glass CSS only)

Frosted glass is backdrop-filter. Liquid glass reads as a thick, lit pane, and three ingredients do that: tint, blur with saturation, and a rim built from inset shadows. This block works in every current browser:

.liquid-glass {
  position: relative;
  isolation: isolate;          /* keeps the ::after's z-index inside this box */
  padding: 28px;
  border-radius: 28px;
  color: #fff;

  /* 1. tint */
  background: rgba(255, 255, 255, 0.10);

  /* 2. frost — blur + saturation + a touch of brightness */
  backdrop-filter: blur(8px) saturate(180%) brightness(1.1);
  -webkit-backdrop-filter: blur(8px) saturate(180%) brightness(1.1);

  /* 3. specular rim: strong top highlight, softer on the other edges */
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow:
    0 8px 32px rgba(0, 0, 0, 0.25),                 /* lift */
    inset 0 1px 1px rgba(255, 255, 255, 0.55),      /* top */
    inset 0 -1px 1px rgba(255, 255, 255, 0.30),     /* bottom */
    inset 1px 0 1px rgba(255, 255, 255, 0.20),      /* left */
    inset -1px 0 1px rgba(255, 255, 255, 0.20);     /* right */
}

Then add the diagonal sheen with a pseudo-element, so the surface looks glossy rather than matte:

.liquid-glass::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;                 /* below the text, above the tint */
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    135deg,
    rgba(255, 255, 255, 0.45) 0%,
    rgba(255, 255, 255, 0.08) 28%,
    transparent 58%
  );
  mix-blend-mode: screen;
}

Why the asymmetric rim? Real glass catches light from above, so the top inset is roughly twice as bright as the sides. Make all four equal and the card flattens into an outlined box. If you want to tune the lift shadow and the inset highlights visually, the box-shadow generator builds layered inset shadows and copies the whole declaration.

A complete, working liquid glass card then looks like this:

<div class="liquid-glass">
  <h3>Liquid glass</h3>
  <p>Frosted, lit from the top, glossy across the surface.</p>
</div>

Put it over a busy, colorful background — a photo, or a few overlapping radial gradients. Over a flat fill the blur has nothing to sample and the effect vanishes (see Common mistakes below).

2. Light-mode variant

On a light background, white tint plus white highlights disappear. Flip the tint toward the background color, lower the saturation boost, and switch the rim to a mix of white (top) and dark (bottom) so the edge still reads:

.liquid-glass.light {
  color: #111;
  background: rgba(255, 255, 255, 0.45);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border: 1px solid rgba(255, 255, 255, 0.65);
  box-shadow:
    0 8px 24px rgba(0, 0, 0, 0.10),
    inset 0 1px 1px rgba(255, 255, 255, 0.90),
    inset 0 -1px 1px rgba(0, 0, 0, 0.06);
}

.liquid-glass.light::after {
  background: linear-gradient(
    135deg,
    rgba(255, 255, 255, 0.70) 0%,
    rgba(255, 255, 255, 0.15) 30%,
    transparent 60%
  );
  mix-blend-mode: normal;      /* screen does nothing useful over white */
}

Two rules of thumb for light mode: more blur (12–16px) because light backgrounds have less contrast to frost, and a higher tint alpha (0.4–0.6) because the text needs a more opaque surface to stay readable. If you theme with tokens, drop your brand color into the color converter to get it as rgb() so you can append an alpha: rgb(14 165 233 / 0.12) makes a tinted glass in one line.

3. Real refraction with SVG displacement

To make the backdrop actually bend, displace it with an SVG filter. Procedural noise from feTurbulence drives an feDisplacementMap:

<svg width="0" height="0" aria-hidden="true" style="position:absolute">
  <filter id="liquid" x="-20%" y="-20%" width="140%" height="140%">
    <feTurbulence type="fractalNoise" baseFrequency="0.008 0.012"
                  numOctaves="2" seed="9" result="noise" />
    <feGaussianBlur in="noise" stdDeviation="1.4" result="soft" />
    <feDisplacementMap in="SourceGraphic" in2="soft" scale="40"
                       xChannelSelector="R" yChannelSelector="G" />
  </filter>
</svg>
@supports (backdrop-filter: url('#liquid')) {
  .liquid-glass {
    backdrop-filter: blur(4px) saturate(180%) brightness(1.1) url('#liquid');
  }
}

How it works: feTurbulence produces an RGB noise image. feDisplacementMap reads the red channel to shift each backdrop pixel horizontally and the green channel to shift it vertically. A channel value of 0.5 means “no move”, so values above and below push pixels in opposite directions, and scale sets how far (in user units) a full-strength value can move a pixel. The feGaussianBlur in between softens the noise so the distortion looks like water, not static.

Tuning the three numbers

AttributeLow valuesHigh valuesGood range for glass
baseFrequencyLarge, slow waves (0.002 — a single swell across the card)Fine ripples (0.05+ — looks like frosted texture, not liquid)0.005–0.015; use two values (x y) for a slightly stretched, less “random” wobble
numOctaves1 — smooth, blobby4+ — adds fine detail on top of the big waves, much more expensive1–2
scale5–15 — barely visible shimmer80+ — the backdrop tears into chunks20–50

Change seed to get a different noise layout with the same character. The filter region (x="-20%" etc.) is widened on purpose: displacement pulls pixels in from outside the element, and with the default region the edges clip to hard lines.

A static alternative: feImage

Noise gives you an organic, random wobble. Apple’s real effect is not random: the distortion is strongest at the edges (like a lens) and flat in the middle. You get that by replacing the turbulence with a hand-made displacement map — a PNG or inline SVG where the red/green channels encode the push direction, grey in the center, ramping toward the edges:

<filter id="lens" x="0" y="0" width="100%" height="100%">
  <feImage href="lens-map.png" preserveAspectRatio="none"
           result="map" />
  <feDisplacementMap in="SourceGraphic" in2="map" scale="32"
                     xChannelSelector="R" yChannelSelector="G" />
</filter>

The map is stretched to the element with preserveAspectRatio="none", so one image fits any card size. Paint it with a radial gradient from rgb(128,128,128) in the middle to red/green-shifted corners, or generate it from a small canvas script. This is the technique the in-depth walkthroughs at LogRocket and this Habr article build up to, and it is much closer to the native look than raw noise.

Browser support

Two separate features matter: backdrop-filter itself, and whether a browser accepts an SVG filter reference (url(#id)) inside backdrop-filter.

FeatureChrome / EdgeSafariFirefox
backdrop-filter (blur, saturate…)76+9+ with -webkit- prefix, 18+ unprefixed103+
@supports (backdrop-filter: …)yesyesyes
url(#svg-filter) inside backdrop-filteryesno (declaration is dropped)not reliable — treat as no
url(#svg-filter) inside plain filteryesyesyes
prefers-reduced-transparency media query118+nobehind a flag

The practical consequence: keep -webkit-backdrop-filter for older Safari, and always gate the SVG refraction behind @supports (backdrop-filter: url('#id')). Browsers that fail the check keep the frosted layer, which already looks good. Do not combine the two in one unguarded declaration — Safari rejects the whole backdrop-filter value if any part of it is invalid, and you lose the blur too.

Accessibility

  • Contrast is unpredictable. Text over glass sits on whatever scrolls behind it. WCAG’s 4.5:1 ratio must hold against the worst part of the backdrop, not the average. Raise the tint alpha, add a subtle text-shadow: 0 1px 2px rgb(0 0 0 / .4), or keep glass for chrome (toolbars, pills) and put long text on solid surfaces.
  • Respect reduced transparency. Users who turn on “Reduce transparency” in macOS or iOS expect opaque panels:
@media (prefers-reduced-transparency: reduce) {
  .liquid-glass {
    background: rgb(28 30 38 / 0.92);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}
  • Reduced motion. If you animate the displacement (for instance by animating baseFrequency with an SVG animate element), disable it under prefers-reduced-motion: reduce. A rippling backdrop behind text is a real problem for readers with vestibular disorders.

Performance

backdrop-filter forces the browser to composite everything behind the element into a texture, blur it, and re-composite every frame something behind it moves. Three rules keep it smooth:

  1. Keep it off scrolling lists. One glass header over a scrolling page is fine; 50 glass cards inside a scrolling list means 50 blurs per frame. Use glass for the container, not the items.
  2. Blur radius is the cost knob. blur(8px) is cheap; blur(40px) on a full-width panel can drop a mid-range phone below 60fps. Liquid glass wants a small blur (4–10px) anyway — the saturation does the work.
  3. SVG displacement is the expensive layer. feTurbulence with numOctaves="2" recomputed every frame on a large element is noticeably heavier than any CSS filter. Keep displaced elements small (buttons, pills, a single card), keep numOctaves at 1–2, and never animate the noise on more than one element.

will-change: backdrop-filter is rarely useful: the element is already promoted to its own compositor layer by the filter. Adding will-change: transform to a glass element that moves (a draggable card, a slide-in sheet) does help, because it stops the browser re-rasterizing the pane while it moves.

Common mistakes

  • Flat background. The most frequent “it doesn’t work” report. Glass only shows over variation. Test over a photo or stacked radial gradients first.
  • Forgetting isolation: isolate. The sheen uses z-index: -1. Without its own stacking context, the pseudo-element drops behind the parent’s background and disappears — or, worse, behind the page.
  • ::after covering the text. If you set z-index: 1 instead, the gloss sits on top of the text and washes it out. Keep the sheen at -1 and the text at auto.
  • Missing position: relative. The inset: 0 on the pseudo-element is relative to the nearest positioned ancestor; without it the sheen stretches over the whole page.
  • overflow: hidden on the glass element. It clips the outer lift shadow. Use border-radius: inherit on children instead.
  • Equal highlights on all sides. Reads as an outline, not a rim. Keep the top highlight the strongest.
  • Blur too strong. Beyond ~16px the backdrop turns into a uniform smear and the material reads as “frosted plastic”, not glass.

FAQ

Can I do liquid glass with CSS only, no SVG?

Yes. Tint, blur, saturation, the specular rim and the sheen are all plain CSS and work in every current browser. You only lose the refraction (the visible bending of the backdrop), which is the one part that needs an SVG feDisplacementMap and currently works only in Chromium.

What is the difference between glassmorphism and liquid glass?

Glassmorphism is a translucent tint plus a blur and a thin border. Liquid glass adds strong saturation, an asymmetric specular rim, a glossy sheen and edge refraction so the panel looks like a thick lens rather than frosted film. Compare the glassmorphism card and the liquid glass card side by side.

Why does my backdrop-filter not work in Safari?

Older Safari needs the -webkit-backdrop-filter prefix (unprefixed support arrived in Safari 18). Also, if any part of the value is unsupported — such as url(#filter) — Safari discards the whole declaration, so put the SVG part inside @supports.

Does liquid glass hurt performance?

A single glass element with a small blur is cheap. The cost scales with the element’s area, the blur radius, and how often what is behind it changes. Avoid glass on many list items, keep the blur under ~10px, and reserve the SVG displacement for small controls.

How do I make the glass tinted with my brand color?

Replace the white tint with your color at low alpha, for example background: rgb(14 165 233 / 0.14), and keep the highlights white. Use the color converter to get your hex as space-separated rgb() so you can add the alpha channel.

Try it live

Open the liquid glass card snippet and drag the blur, saturation, radius and tint controls, then copy the CSS with your settings. Browse all snippets or subscribe via RSS.