Liquid Glass in React and Tailwind: What Works in Chrome, Safari and Firefox

7 min read

TL;DR Use bg-white/10, a light border and backdrop-blur + backdrop-saturate for the frosted layer in every browser. backdrop-filter: url(#svg-filter) refraction renders only in Chromium, yet Firefox and WebKit report it as supported, so gate it with a Chromium check via useSyncExternalStore, keep blur in its own rule, and add a prefers-reduced-transparency fallback.

On this page
  1. What is liquid glass made of in CSS?
  2. How do you add refraction with an SVG filter?
  3. Which browsers render backdrop-filter: url()?
  4. Why can't @supports detect it?
  5. Does the Chromium check cause a hydration error?
  6. What about prefers-reduced-transparency?
  7. Is liquid glass slow?
  8. What I'd ship
  9. FAQ
  10. Does backdrop-filter work in Safari?
  11. Why does my liquid glass work in Chrome but not Firefox?
  12. Can I use @supports to detect SVG backdrop filters?
  13. How do I make liquid glass with Tailwind?
  14. Should glass respect reduced transparency?
  15. Sources

Liquid glass in React comes in two layers. The frosted part is a translucent background with backdrop-filter: blur() saturate(), a thin light border and an inner highlight, and it works in Chrome, Safari and Firefox. The part that bends what's behind the glass needs an SVG filter applied through backdrop-filter: url(#filter), and that only renders in Chromium. Firefox and WebKit still report the url() value as supported, so @supports can't gate it. Keep blur as the default everywhere, switch the SVG filter on only in Chromium, and give people who ask for less transparency a solid background.

I make qodesign, a set of React + TypeScript components with Tailwind (most also use Framer Motion). Our Liquid Glass Tab Bar uses the frosted layer, so I wanted to know exactly where the refraction layer works before telling anyone to add it. On October 21, 2026 I built a test page in a fresh Next 16.4.2 app (React 19.3.0, Tailwind v4), ran tsc and next build, and loaded the production build in headless Chromium 156, Firefox 157 and Playwright's WebKit 27.2. Every panel sat on top of vertical red, yellow, blue and black stripes, so blur and bending both show up as measurable changes in the pixels.

What is liquid glass made of in CSS?

The frosted layer is four things: a background that is mostly transparent, a backdrop blur, a saturation boost so the colours behind look richer, and a light edge. These are the values from our tab bar as Tailwind classes:

tsx
export function GlassBar({ children }: { children: React.ReactNode }) {
  return (
    <nav className="rounded-[32px] border border-white/15 bg-white/10 p-2 shadow-[inset_0_1px_0_rgba(255,255,255,.25),0_10px_30px_rgba(0,0,0,.35)] backdrop-blur-[16px] backdrop-saturate-[1.9]">
      {children}
    </nav>
  );
}

MDN's backdrop-filter page points out that the element or its background has to be transparent or partly transparent for the effect to show. In practice glass also needs something busy behind it. Over a flat colour it just looks grey.

In Chromium, the blur(16px) panel cut the edge contrast of the stripes behind it from 3.59 to 1.99 (average brightness change between neighbouring pixels, on a 0 to 255 scale).

The liquid glass effect guide has the same values with the full tab bar.

Liquid Glass Tab Bar running live. A glass drop magnifies the tab under it, stretches between tabs and bursts into the player.

How do you add refraction with an SVG filter?

Apple's version bends the content at the edges of the glass. On the web the usual way to get close is an SVG filter with feTurbulence or a displacement image feeding feDisplacementMap, referenced from backdrop-filter. Here's the component I tested. It renders its own filter with an id from useId, so two panels on one page don't share a filter:

tsx
"use client";

import { useId, useSyncExternalStore, type CSSProperties, type ReactNode } from "react";

type Props = {
  children: ReactNode;
  className?: string;
  scale?: number;
  refract?: "auto" | "on" | "off";
};

// SVG filters inside backdrop-filter only render in Chromium. CSS.supports() says
// yes in Firefox and Safari too, so check for a Chromium-only API instead.
function isChromium() {
  const uaData = (navigator as Navigator & { userAgentData?: { brands: { brand: string }[] } }).userAgentData;
  return !!uaData?.brands.some((b) => b.brand === "Chromium");
}
const subscribe = () => () => {};

export function LiquidGlass({ children, className = "", scale = 60, refract = "auto" }: Props) {
  const id = `lg-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
  const chromium = useSyncExternalStore(subscribe, isChromium, () => false);
  const on = refract === "on" || (refract === "auto" && chromium);
  const style = { "--lg-filter": `url(#${id})` } as CSSProperties;

  return (
    <div style={style} className={`liquid-glass ${on ? "lg-on" : ""} ${className}`}>
      <svg aria-hidden="true" width="0" height="0" className="absolute">
        <filter id={id} x="0" y="0" width="100%" height="100%">
          <feTurbulence type="fractalNoise" baseFrequency="0.008 0.008" numOctaves="2" seed="7" result="noise" />
          <feGaussianBlur in="noise" stdDeviation="2" result="soft" />
          <feDisplacementMap in="SourceGraphic" in2="soft" scale={scale} xChannelSelector="R" yChannelSelector="G" />
        </filter>
      </svg>
      {children}
    </div>
  );
}

And the CSS. The blur is the default, and the url() only goes on when the lg-on class is there:

css
.liquid-glass {
  position: relative;
  background: rgb(255 255 255 / 0.08);
  border: 1px solid rgb(255 255 255 / 0.18);
  -webkit-backdrop-filter: blur(2px) saturate(1.6);
  backdrop-filter: blur(2px) saturate(1.6);
}
.liquid-glass.lg-on {
  backdrop-filter: var(--lg-filter) blur(2px) saturate(1.6);
}
@media (prefers-reduced-transparency: reduce) {
  .liquid-glass,
  .liquid-glass.lg-on {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgb(20 20 24 / 0.92);
  }
}

Use it like any wrapper: <LiquidGlass className="rounded-[32px] p-4">…</LiquidGlass>. Raise scale for stronger bending. Turbulence gives a wobbly, watery look. For the clean lens-edge look, swap it for an feImage displacement map.

On the useId point: React 19.3 gave me ids like _R_6lubrb_, which already work inside url(#…). I tested the raw id and the cleaned one and both rendered the same. The replace costs nothing and protects you if the format changes again.

Which browsers render backdrop-filter: url()?

Here's what I measured. With vertical stripes behind a panel, every row of pixels is identical, so the average change between rows is 0. Blur keeps it at 0. Anything that bends the stripes makes it go up.

EngineBending under the SVG panelPlain blur() visible in my run
Chromium 156yes, row change 0.52 (0 without the filter)yes
Firefox 157, headlessnono
WebKit 27.2 (Playwright), headlessnono

Chromium is clear. For the other two I can't lean on my screenshots: on this headless Linux setup even plain blur() didn't show up, so the missing bend proves nothing by itself. The public bug trackers fill the gap. WebKit's bug 245510, "backdrop-filter: url(#some-svg-filter) doesn't work with SVG filters like feDisplacementMap", was opened in September 2022 and was still marked NEW when I checked. Mozilla's bug 1961378 is titled "backdrop-filter gets ignored if it would fall back to a blob image (such like with some SVG url filters)", also NEW.

That Firefox title is the reason the CSS above keeps url() out of the default rule. If Firefox ignores the whole declaration, you lose your blur too and the glass turns into a faint tint. With the split rules, Firefox and Safari keep the frosted layer whatever happens.

Why can't @supports detect it?

The usual move would be @supports (backdrop-filter: url(#x)). I checked what each engine answers:

CheckChromium 156Firefox 157WebKit 27.2
CSS.supports('backdrop-filter', 'url(#x)')truetruetrue
CSS.supports('backdrop-filter', 'url(#x) blur(2px)')truetruetrue
computed value keeps url(...)yesyesyes

All three accept the syntax. Parsing it and rendering it are different things, so @supports gives you a yes in exactly the browsers you were trying to filter out.

What I used instead is navigator.userAgentData. MDN's compatibility data lists it in Chrome and Edge from version 90 and not in Firefox or Safari, and its brands list includes "Chromium" in Chromium-based browsers. That's a browser check, and I'd normally avoid one, but here it matches the one fact that matters: who renders SVG backdrop filters today. When WebKit or Firefox fix their bugs, you change one function.

Does the Chromium check cause a hydration error?

It would if you read navigator during render, because the server has no navigator. useSyncExternalStore with a server snapshot of false avoids that. The server HTML and the first client render both have no lg-on class, then React switches to the real value after hydration.

In my production build the refract="auto" panel got lg-on in Chromium and didn't in Firefox or WebKit, with 0 page errors in all three. Its computed backdrop-filter in Chromium was url("#lg-_R_clubrb_") blur(2px) saturate(1.6), and in the other two it stayed blur(2px) saturate(1.6).

What about prefers-reduced-transparency?

Some people turn on "Reduce transparency" in their OS settings, and glass is the first thing that setting is meant to remove. The prefers-reduced-transparency media query lets you respect it. MDN marks it experimental with limited availability: its compatibility data lists Chrome from 118, Firefox behind a flag, and no Safari support.

I emulated the setting in Chromium through DevTools. The query matched, the panel's backdrop-filter became none and its background became rgba(20, 20, 24, 0.92), which is the fallback from the CSS above. Pick a fallback colour that keeps your text readable without any blur. For the same idea applied to animation, see the prefers-reduced-motion guide.

Is liquid glass slow?

I didn't measure it for this post, and a headless browser with a software renderer wouldn't give honest numbers anyway. The tab bar's own guide notes that backdrop blur costs GPU time, which is why the bar switches its blur off while the lens and player states are open. If you animate a glass panel, test on a real phone and keep the number of blurred layers on screen small.

What I'd ship

Ship the frosted layer everywhere. Add the SVG refraction as an extra for Chromium, behind a check that doesn't trust @supports. Add the reduced-transparency fallback. If you want a finished piece, the animated tab bar guide and the Liquid Glass Tab Bar use the frosted layer with a magnifying lens made from a scaled copy of the bar, which looks the same in every browser.

Not tested: real Safari on macOS or iOS, Firefox with a real GPU, Edge, and frame rates on phones. All runs were headless on Linux.

FAQ

Does backdrop-filter work in Safari?

Yes for filter functions like blur() and saturate(). MDN's compatibility data lists backdrop-filter in Safari 18 and with the -webkit- prefix since Safari 9, so keep the prefixed line for older versions. SVG filters through backdrop-filter: url() are a different story: WebKit bug 245510 for that was still open on October 21, 2026.

Why does my liquid glass work in Chrome but not Firefox?

If your glass uses backdrop-filter: url(#filter), only Chromium renders it. Mozilla's bug 1961378 says Firefox can ignore the whole backdrop-filter when an SVG url filter is involved, which would also drop your blur. Keep blur() in a separate default rule and add the url() only for Chromium.

Can I use @supports to detect SVG backdrop filters?

No. In my test Chromium 156, Firefox 157 and WebKit 27.2 all returned true for CSS.supports('backdrop-filter', 'url(#x)'). Checking navigator.userAgentData for the "Chromium" brand worked, with useSyncExternalStore to avoid a hydration mismatch.

How do I make liquid glass with Tailwind?

Use bg-white/10 border border-white/15 backdrop-blur-[16px] backdrop-saturate-[1.9] plus an inset shadow for the highlight, on top of something colourful. For the refraction layer, use a small CSS class that adds url(#id) to the backdrop filter, because the id comes from useId at runtime, and only turn it on in Chromium.

Should glass respect reduced transparency?

Yes. Add @media (prefers-reduced-transparency: reduce) and swap the blur for a nearly solid background. Chromium supports the query from version 118 according to MDN's data, and browsers without it simply ignore the rule.

Sources