Перейти к содержимому
Alex Kovalev

Как grayscale-фильтр сломал все мои попапы

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

2мин чтения

Ставите 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
100200300400500600700<html><footer><body>свой слойвьюпортПодписаться на рассылку?Давайcontaining block: вьюпорт
На <body>

Привязаны к стеклу. Крутите сколько угодно.

Так же падают от perspective, backdrop-filter, contain: paint.

Проблема, которую они решают Представьте такой сценарий:

<div style="filter: blur(10px);">
<p>Этот текст должен быть размытым</p>
<div style="position: fixed;">А этот должен быть размыт?</div>
</div>

Если бы fixed-элемент оставался привязан к вьюпорту, он бы:

  1. Рендерился вне отфильтрованного слоя
  2. Не получал эффект размытия
  3. Ломал визуальную иерархию (потомок выходит из-под визуального эффекта родителя)
Выпустите fixed-ребёнка из размытия
viewport

Этот текст должен быть размытым

Fixed-ребёнок, запаянный в размытый div.

Размытие убавлено с 10px, чтобы текст ещё читался.

Вы можете не считать эти причины проблемой, но для браузеров и w3c это проблема, потому что это идёт вразрез с философией каскада в CSS. Технически можно было бы сделать исключение, но оно потребовало бы более затратного рендеринга.

Как это исправить?

Исправить проблему можно двумя способами:

  1. убрать со родительского элемента стиль, который делает его containing block
  2. указать позицию для 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.

Чирикну, когда выйдет новый пост

Одно письмо на каждый новый пост, обычно по пятницам. Больше ничего, а отписаться можно в любой момент.

Удобнее читать в RSS-ридере? Есть RSS-лента.

© Alex Kovalev. Все права защищены. · Политика конфиденциальности