Усі нотатки

Меню, яке показувало один пункт: Safari й backdrop-filter

Тести зелені, а на iPhone у меню лише «Роботи». Розбираю, чому filter і backdrop-filter ламають position: fixed, і як ловити це тестом.

(╯°□°)╯︵ ┻━┻ 28 вересня 2026 р. · 3 хв читання CSSSafariтести

Сайт готовий. Скрипт перевірки пройшов 21 сторінку в трьох ширинах і двох темах, клацнув мобільне меню, перевірив фільтр робіт. «Проблем немає». Я публікую реліз і йду по чай .

За п’ять хвилин приходить скриншот з iPhone: у повноекранному меню — один-єдиний пункт. «Роботи». Решти немає. Не сховані, не за краєм — їх просто не видно.

Очікування
Headless Chromium: меню відкривається, пунктів чотири, панель на весь екран, тест зелений.
Реальність
Safari на iPhone: меню відкривається… на висоту шапки. 64 пікселі щастя. «Роботи».

Що сталося

Шапка сайту напівпрозора й розмиває те, що під нею: backdrop-filter: blur(14px). Гарно. Мобільне меню — це панель position: fixed на весь екран, і в розмітці вона лежить усередині шапки.

А тепер рядок зі специфікації, який варто татуювати на руці фронтендера: якщо предок має filter, backdrop-filter, transform, perspective, contain: paint чи will-change: transform, він стає контейнером для fixed-нащадків. Тобто position: fixed рахується вже не від вікна, а від цього предка.

Моя «на весь екран» панель стала «на всю шапку»: її розміри й положення тепер рахувались від смужки заввишки 64 пікселі. З усього меню лишився видимим перший пункт.

/* було: розмиття на самій шапці — і fixed-панель усередині «живе» в її межах */
.site-header { position: sticky; backdrop-filter: saturate(1.4) blur(14px); }
.drawer__panel { position: fixed; inset: 64px 0 0 0; }

Виправлення на три рядки

Розмиття не обов’язково вішати на саму шапку. Досить псевдоелемента, який лежить під її вмістом:

.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);
}

Шапка так само матова, але фільтр тепер на сусідньому елементі, а не на предку панелі. Меню знову на весь екран, і в ньому всі шість пунктів.

Чому тест не впіймав

Найцікавіше не в самій ваді, а в тому, чому тест був зелений. Наш headless Chromium не підтримує backdrop-filter. Правило стоїть під @supports (backdrop-filter: blur(1px)), тож у тестовому браузері фільтра просто не було, а значить і вади. Тест чесно перевіряв сайт, якого ніхто з людей не бачить.

Висновок: мало перевіряти, що «пункт меню видно». Треба перевіряти геометрію й причини:

// панель має бути справді на весь екран
const h = await page.locator('.drawer__panel').evaluate((e) => e.getBoundingClientRect().height);
if (h < 500) bad(`панель заввишки лише ${h}px`);

// жоден предок fixed-панелі не має 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;
});

Друга перевірка ловить ваду навіть у браузері, який сам її не відтворює: вона дивиться на CSS, а не на картинку. Тепер вона живе в наборі перевірок сайту, і повторити цей трюк уже не вийде.

Коротко

  • filter, backdrop-filter, transform, perspective, contain, will-change на предку змінюють, від чого рахується position: fixed.
  • Розмиття фону — на псевдоелемент, а не на контейнер із fixed-дітьми.
  • Тестуйте причини, а не лише симптоми: headless-браузер може не мати тієї самої властивості, що ламає сайт у людей.
  • І завжди відкривайте реліз на справжньому телефоні. Скриншот від власника — найчесніший тест .

Маєте ідею? Давайте поговоримо

Опишіть задачу кількома реченнями — відповім з питаннями й першою оцінкою.