Как grayscale-фильтр сломал все мои попапы
Почему добавление стиля filter к элементу body ломает fixed-элементы?

Ставите position: fixed на элемент — к чему он прилипает? К вьюпорту, очевидно. Кроме тех случаев, когда ваш плагин доступности добавляет к body фильтр в градациях серого.
Оказывается, fixed-элементы не всегда закреплены относительно вьюпорта. Они закреплены относительно своего containing block. Обычно это и есть вьюпорт. Но некоторые CSS-свойства — например, filter — создают новый containing block.
Так что когда .pojo-a11y-grayscale навешивается на ваш body, каждый fixed-элемент теперь позиционируется относительно <body>, а не вьюпорта. Ваша прилипающая навигация, модалки и кнопки в правом нижнем углу проваливаются в самый низ страницы, прямо под футер. Они всё ещё “fixed”, просто зафиксированы не относительно того, чего надо.
- фильтр на
- ни на чём
- containing block
- вьюпорт
- кнопка чата
- на экране
Отметьте «Оттенки серого». И следите за правым нижним углом.
body.pojo-a11y-grayscale { filter: grayscale(100%); -webkit-filter: grayscale(100%); -webkit-filter: grayscale(1); -moz-filter: grayscale(100%); -ms-filter: grayscale(100%); -o-filter: grayscale(100%); filter: gray;}На самом деле, любое из указанных ниже правил вызовет тот же эффект
body { transform: translateZ(0); will-change: transform; perspective: 1000px; filter: grayscale(100%);}
Но почему?
Необходимость для конвейера рендеринга
Когда браузер применяет filter или transform, ему нужно создать изолированный слой рендеринга. Всё внутри этого слоя — включая fixed-элементы — должно быть частью одного и того же визуального эффекта.
position: fixedПривязаны к стеклу. Крутите сколько угодно.
perspective, backdrop-filter, contain: paint.Проблема, которую они решают Представьте такой сценарий:
<div style="filter: blur(10px);"> <p>Этот текст должен быть размытым</p> <div style="position: fixed;">А этот должен быть размыт?</div></div>Если бы fixed-элемент оставался привязан к вьюпорту, он бы:
- Рендерился вне отфильтрованного слоя
- Не получал эффект размытия
- Ломал визуальную иерархию (потомок выходит из-под визуального эффекта родителя)
Этот текст должен быть размытым
Fixed-ребёнок, запаянный в размытый div.
Вы можете не считать эти причины проблемой, но для браузеров и w3c это проблема, потому что это идёт вразрез с философией каскада в CSS. Технически можно было бы сделать исключение, но оно потребовало бы более затратного рендеринга.
Как это исправить?
Исправить проблему можно двумя способами:
- убрать со родительского элемента стиль, который делает его
containing block - указать позицию для fixed-элементов относительно нового containing block
Вернёмся к WordPress-сайту с плагином Pojo Accessibility. По клику он добавляет к body .pojo-a11y-grayscale — тот самый блок из начала поста. Всё после его первой строки — это вендорные префиксы и устаревший синтаксис для браузеров, которых почти не осталось, так что ниже я пропущу эту стену и напишу просто filter. Если вам всё ещё есть дело до древних браузеров, верните префиксы на место.
Я бы переопределил это вот так:
body.pojo-a11y-grayscale { filter: unset;}
html:has(body.pojo-a11y-grayscale) { filter: grayscale(100%);}Плюс в том, что мы не трогаем JS, не меняем работу плагина или его триггеров, и всё явно. Эффект просто переезжает с body на html. А html — единственное исключение в спецификации: фильтр на корневом элементе не создаёт новый containing block, так что все fixed-элементы возвращаются к вьюпорту.
body.pojo-a11y-grayscale {
filter: grayscale(100%);
}Как делает плагин. Body хватает попап, и тот за экраном.
filter; transform на html всё равно хватает.Другой вариант
— указать соседей вместо предка. Если у вас структура вроде такой
<html> <body> <div style="position:fixed">...</div> <article>...</article> </body></html>можно применить фильтр на уровне соседей fixed-блока вот так:
/* Убираем фильтр плагина с body */body.pojo-a11y-grayscale { filter: unset;}
body.pojo-a11y-grayscale > * { filter: grayscale(100%);}Так или иначе, попапы выбираются обратно из-под футера. В следующий раз, когда что-то с position: fixed перестанет быть fixed, не дебажьте сам элемент. Пройдитесь вверх по его предкам и поищите filter, transform, perspective или will-change. Скорее всего, один из них нагло занял место вашего вьюпорта.
Смотрите также: Чиню шрифт, ещё один CSS-баг на ещё одном старом сайте на WordPress.
Комментарии
Комментарии живут в GitHub Discussions (через giscus), так что для участия нужен аккаунт GitHub.