The backdrop-filter that ate my fixed nav | WaretaGarasuSkip to main content
WaretaGarasu
Language
Theme
Glass
All writingNote · CSS

The backdrop-filter that ate my fixed nav

A blurred sticky header quietly trapped a position: fixed overlay inside it. Here's why, and the one-line mental model that explains it.

CSSbackdrop-filterDebugging

The symptom

I had a sticky site header with a blurred background and a full-screen mobile nav panel set to position: fixed; inset: 0. On desktop everything was fine. On mobile the panel refused to cover the viewport: it was clipped to the header's box, sitting in a thin strip at the top instead of filling the screen.

No overflow: hidden anywhere. No transform on an ancestor. The markup was correct. The panel was a child of the header for accessibility reasons, and that turned out to be the entire problem.

Why it happens

position: fixed is positioned relative to the viewport, usually. The exception is when an ancestor establishes a containing block. transform, filter, perspective, will-change, and contain all do this. So does backdrop-filter.

The moment you put backdrop-filter on the header, the header becomes the containing block for every fixed descendant. inset: 0 stops meaning the viewport and starts meaning the header's padding box.

.site-header {
  position: sticky;
  top: 0;
  backdrop-filter: blur(10px); /* ← creates a containing block */
}

/* The panel wants to cover the viewport... */
.nav-panel {
  position: fixed;
  inset: 0; /* ...but inset is now relative to .site-header, not the viewport */
}

The fix

Keep the blur off the ancestor chain of the fixed element. Move backdrop-filter onto an absolutely positioned ::before pseudo-element on the header. The pseudo-element blurs what's behind the header, but it is not an ancestor of the nav panel, so it never establishes a containing block for it.

/* Keep the blur, but on a pseudo-element that isn't an ancestor
   of the fixed panel. The header no longer establishes the
   containing block, so position: fixed snaps back to the viewport. */
.site-header {
  position: sticky;
  top: 0;
}

.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  backdrop-filter: blur(10px);
}

The header looks identical. The panel goes back to covering the viewport. Same visual result, no trap.

The takeaway

Whenever a position: fixed element is clipped to the wrong box, stop inspecting the element and start walking up the tree for the containing-block triggers: transform, filter, backdrop-filter, perspective, will-change, contain. One of them is on an ancestor. The element is behaving exactly as specified. The surprise is that a purely visual property carries a layout side effect.