← Writing// post

JUNE 12, 2026 · 5 MIN READ

Turbopack quietly ate my backdrop-filter

I shipped liquid-glass cards and the blur never arrived. The CSS was right, the browser was capable — the build pipeline was eating the declaration. A debugging story.

Next.jsCSSDebugging

I was shipping liquid-glass cards on this site — translucent surfaces with a gradient rim and a real backdrop-filter blur, so the page behind them refracts like frosted glass. The CSS was textbook:

html[data-perf='full'] .lq {
  background: linear-gradient(145deg, rgba(255,255,255,0.06), ...);
  backdrop-filter: blur(16px) saturate(1.5);
  -webkit-backdrop-filter: blur(16px) saturate(1.5);
}

The cards rendered. The gradient rendered. The blur never arrived. No console error, no build warning — just glass that wasn't glass.

Ruling out the obvious

First suspicion is always your own selector. So I asked the browser what it actually computed:

getComputedStyle(card).backdropFilter  // → "none"

Then whether the browser even supports it:

CSS.supports('backdrop-filter', 'blur(16px)')  // → true
card.style.backdropFilter = 'blur(16px) saturate(1.5)'
getComputedStyle(card).backdropFilter  // → "blur(16px) saturate(1.5)" ✓

Inline styles worked. The stylesheet didn't. That narrows it to exactly one place: whatever sits between my CSS file and the browser.

Reading the stylesheet the browser actually received

The trick that cracked it was dumping the rule from the CSSOM — not my source file, but what survived the build:

for (const sheet of document.styleSheets)
  for (const rule of sheet.cssRules)
    if (rule.selectorText?.includes('.lq'))
      console.log(rule.cssText)

And there it was:

html[data-perf="full"] .lq {
  background: linear-gradient(145deg, rgba(255, 255, 255, 0.06), ...);
}

The background made it through. Both backdrop-filter declarations were gone — stripped from the rule entirely. Even better: an older .glass utility from a previous design had been silently blur-less for months. Nobody noticed, because a translucent dark card without blur still looks plausible.

The culprit

This project runs Next.js 16 with Turbopack, and Turbopack processes CSS with Lightning CSS. Lightning CSS transforms your stylesheet against a browser-support matrix — and in my setup (helped along by months-old caniuse-lite data in the lockfile), it decided backdrop-filterwasn't shippable and removed the declarations instead of prefixing them. Silently. The dev server and the production build both did it, which is exactly why it looked like a CSS bug rather than a pipeline bug.

The takeaway that stung: "the browser supports it" and "your build ships it" are two different facts, and the only way to verify the second one is to inspect the CSSOM the browser actually parsed.

The fix (which ended up better than the original)

I could have fought the toolchain — updated browserslist data, pinned targets, hoped the next Turbopack release behaved. Instead I moved the blur out of the stylesheet entirely. This site already has a perf provider that benchmarks the machine on load (frame-rate sample plus core count) and assigns a tier. So the blur is now applied as an inline style, exactly where the tier decision lives:

useEffect(() => {
  document.documentElement.dataset.perf = tier
  const blur = tier === 'full' ? 'blur(16px) saturate(1.5)' : ''
  document.querySelectorAll('.lq').forEach((el) => {
    el.style.backdropFilter = blur
    el.style.setProperty('-webkit-backdrop-filter', blur)
  })
}, [tier])

Inline styles can't be stripped by a CSS pipeline, and as a bonus the expensive effect is now opt-in by hardware: capable machines get real refraction, weak ones get a solid tinted fallback that still reads as glass and costs nothing. The bug forced an architecture I should have chosen anyway.

If your effect "doesn't work" but the CSS looks right