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.
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.
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,containandwill-changeon an ancestor change whatposition: fixedis 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 .