All notes

The menu that showed one item: Safari and backdrop-filter

Tests were green, but on an iPhone the menu showed only “Work”. Why filter and backdrop-filter break position: fixed — and how to catch it in a test.

(╯°□°)╯︵ ┻━┻ 28 September 2026 · 3 min read CSSSafaritesting

The site is ready. The check script went through 21 pages at three widths and two themes, clicked the mobile menu and tried the project filter. “No problems.” I ship the release and go make some tea .

Five minutes later an iPhone screenshot arrives: the full-screen menu contains exactly one item. “Work”. The rest isn’t hidden or off-screen — it just isn’t there.

Expectation
Headless Chromium: the menu opens, four items, full-screen panel, test is green.
Reality
Safari on iPhone: the menu opens… to the height of the header. 64 pixels of joy. “Work”.

What happened

The header is translucent and blurs what’s underneath: backdrop-filter: blur(14px). Pretty. The mobile menu is a full-screen position: fixed panel, and in the markup it lives inside the header.

Now the line from the spec every frontend dev should have tattooed: if an ancestor has filter, backdrop-filter, transform, perspective, contain: paint or will-change: transform, it becomes the containing block for fixed descendants. position: fixed is no longer relative to the viewport — it’s relative to that ancestor.

My “full-screen” panel became “full-header”: its size and position were now computed against a 64-pixel strip. Only the first menu item stayed visible.

/* before: the blur on the header itself — and the fixed panel inside is trapped in it */
.site-header { position: sticky; backdrop-filter: saturate(1.4) blur(14px); }
.drawer__panel { position: fixed; inset: 64px 0 0 0; }

A three-line fix

The blur doesn’t have to sit on the header itself. A pseudo-element beneath its content is enough:

.site-header::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(14px);
}

The header is just as frosted, but the filter is now on a sibling, not on the panel’s ancestor. The menu is full-screen again, all six items present.

Why the test missed it

The interesting part isn’t the bug — it’s why the test was green. Our headless Chromium doesn’t support backdrop-filter. The rule sits under @supports (backdrop-filter: blur(1px)), so in the test browser there was no filter — and no bug. The test honestly checked a site no human ever sees.

The lesson: checking that “a menu item is visible” isn’t enough. Check geometry and causes:

// the panel must really be full-screen
const h = await page.locator('.drawer__panel').evaluate((e) => e.getBoundingClientRect().height);
if (h < 500) bad(`panel is only ${h}px tall`);

// no ancestor of the fixed panel has filter / backdrop-filter / transform
const guilty = await page.locator('.drawer__panel').evaluate((e) => {
  const out = [];
  for (let n = e.parentElement; n; n = n.parentElement) {
    const cs = getComputedStyle(n);
    if (cs.filter !== 'none' || cs.backdropFilter !== 'none' || cs.transform !== 'none') out.push(n.className);
  }
  return out;
});

The second check catches the bug even in a browser that can’t reproduce it — it looks at the CSS, not the pixels. It now lives in the site’s check suite, so this trick won’t work twice.

TL;DR

  • filter, backdrop-filter, transform, perspective, contain and will-change on an ancestor change what position: fixed is relative to.
  • Put background blur on a pseudo-element, not on a container with fixed children.
  • Test causes, not just symptoms: your headless browser may lack the very property that breaks the site for real people.
  • And always open the release on a real phone. A screenshot from the client is the most honest test there is .

Got an idea? Let’s talk

Describe your task in a few sentences — I’ll reply with questions and a first estimate.