12k
All articles

Почему не работает z-index

Разберитесь, почему z-index не работает: найдите контексты наложения в инструментах разработчика, определите блокирующего предка и исправьте модальные окна, меню и закреплённые заголовки.

OpenReplay Team
OpenReplay Team
Почему не работает z-index

Если элемент с z-index: 9999 всё равно отображается под элементом с 2, значит, у одного из предков первого элемента образовался контекст наложения, а z-index никогда не действует за пределами своего контекста.

Вы уже пробовали 99999. Потом 2147483647. Модальное окно по-прежнему под шапкой, а в панели Styles всё выглядит правильно.

Теория порядка наложения изложена в статье CSS: Demystifying the z-index property. Это руководство посвящено отладке: как увидеть контексты наложения в DevTools, как подниматься по DOM, пока не найдёте предка, который «запирает» ваш элемент, и как исправить каждый из типичных случаев, не увеличивая число.

Основные выводы

  • Значение z-index конкурирует только с элементами из того же контекста наложения, поэтому 9999 проигрывает 2, если у предка первого элемента есть собственный контекст наложения.
  • Когда больший z-index проигрывает, сравнивайте предков двух элементов, у которых общий родитель. Собственные значения элементов ничего не решают.
  • Любое значение transform, filter или clip-path, отличное от none, а также opacity меньше 1 создают новый контекст наложения, даже если у элемента нет ни position, ни z-index.
  • Если выпадающее меню обрезается по краю карточки, его, как правило, обрезает overflow, и никакое значение z-index это не исправит.
  • Если сделать оверлей прямым потомком <body>, он выйдет из контекстов наложения всех предков.

Какое единственное правило объясняет, почему z-index не работает?

z-index упорядочивает элемент только относительно других участников его собственного контекста наложения. Контекст наложения — это группа, которую браузер отрисовывает как единое целое. В руководстве MDN по контексту наложения перечислены свойства, которые его создают. Когда предок создаёт контекст, z-index потомка действует только внутри этого предка. Поэтому если предок находится под другим контекстом, потомок тоже останется под ним, какое бы число вы ему ни задали.

Вот минимальный пример этой ошибки:

<header style="position: fixed; top: 0; z-index: 2; background: tomato;">Header</header>
<main style="opacity: 0.99;">
  <div class="modal" style="position: fixed; inset: 0; z-index: 9999; background: rgb(0 0 0 / 0.6);">
    Modal
  </div>
</main>

Модальное окно проигрывает, потому что у main задано opacity: 0.99, из-за чего main становится контекстом наложения. Внутри main значение 9999 у модального окна работает как положено. Но сам main сравнивается с шапкой на корневом уровне, а у шапки 2 — больше. Когда больший z-index проигрывает, смотрите на предков: найдите для каждого элемента ближайшего предка, у которого общий родитель с предком другого элемента, и сравните эти два элемента.

Как увидеть контексты наложения в DevTools?

В DevTools браузеров на базе Chromium есть визуальные инструменты для исследования проблем с наложением — это бывает быстрее, чем гадать. И в Chrome, и в Edge эти представления открываются через Command Menu сочетанием Command+Shift+P в macOS или Control+Shift+P в Windows и Linux. О других малоизвестных панелях DevTools читайте в статье Hidden Gems in Chrome DevTools.

Chrome: панель Layers

  1. Откройте DevTools и нажмите Command+Shift+P или Control+Shift+P.
  2. Введите Layers и выберите Show Layers panel. Также можно перейти в More options > More tools > Layers panel.
  3. Поверните холст и выберите слой, чтобы увидеть подробности, в том числе причину, по которой браузер выделил его в отдельный слой.

Панель Layers показывает слева дерево слоёв страницы, а рядом — их 3D-схему. С её помощью можно изучить, как браузер разбил страницу на композитные слои, но помните, что это не то же самое, что контексты наложения. Чтобы найти предка, отвечающего за контекст наложения, используйте описанный ниже метод подъёма по предкам. Эта панель также пригодится при отладке дёргающихся CSS-анимаций.

Edge: 3D View

  1. Откройте Command Menu и выполните Show 3D View.
  2. Перейдите на вкладку Z-index.
  3. Включите опцию Show all, чтобы видеть позиционированные элементы вместе с контекстами наложения.

В 3D View в Edge три вкладки: Composited Layers, Z-index и DOM. Вкладка Z-index отображает контексты наложения страницы в 3D, поэтому «запертый» элемент будет виден вложенным в более низкую плоскость.

Если вам нужно древовидное представление контекстов наложения в Chrome или Firefox, расширение CSS Stacking Context inspector добавляет панель с деревом контекстов, а также боковую панель в Elements, которая объясняет, почему выбранный элемент создаёт контекст. Согласно документации, заглядывать внутрь сторонних iframe оно пока не умеет.

Метод подъёма по предкам

Метод подъёма позволяет найти «запирающего» предка, используя только панель Elements и вкладку Computed. Он одинаково работает во всех браузерах.

  1. Выберите проигрывающий элемент (модальное окно, меню).
  2. В Elements кликните по его родителю.
  3. Откройте вкладку Computed и по очереди вводите в поле фильтра каждое свойство из чек-листа ниже: transform, opacity, filter, position, z-index и так далее.
  4. Если ни одно из них не создаёт контекст, поднимитесь на уровень выше и повторите, пока не дойдёте до <html>.
  5. Запомните первого предка, который создаёт контекст.
  6. Повторите шаги 1–5, начиная с выигрывающего элемента.
  7. Две цепочки сойдутся на общем предке. Два его потомка, лежащих на этих цепочках, — это и есть элементы, которые на самом деле сравниваются.

Если вы работаете по баг-репорту, а не с локальным воспроизведением, запись сессий (session replay) покажет симптом глазами пользователя: например, клик попадает на невидимый оверлей или меню открывается под контентом. Так вы поймёте, какой элемент выбирать на шаге 1.

Чек-лист: свойства, создающие контекст наложения

Проверьте каждого предка по этой таблице. Достаточно одного совпадения, чтобы «запереть» его потомков.

СвойствоСоздаёт контекст, когдаЧто искать в Computed
position: absolute / relativez-index не равен autoОба значения заданы на одном элементе
position: fixed / stickyВсегдаТолько значение position
opacityМеньше 1Значения вроде 0.99, часто оставшиеся от анимации появления
transform, scale, rotate, translateЛюбое значение, кроме noneЗначение matrix(...), включая тождественные преобразования
filter, backdrop-filterНе noneblur(), drop-shadow()
clip-path, perspectiveНе noneЛюбая фигура или длина
mask, mask-image, mask-borderНе noneИзображения или градиенты
will-changeУказано свойство, которое создало бы контекст, если его задатьwill-change: transform или opacity
isolationisolateЯвно заданное значение
containlayout, paint, strict или contentЧасто встречается в виртуализированных списках
Flex- или grid-элементz-index не равен autodisplay: flex или grid у родителя. position не требуется

В полный список MDN также входят mix-blend-mode со значением, отличным от normal, элементы верхнего слоя (top layer) и элементы, у которых создающие контекст свойства анимируются с animation-fill-mode: forwards. MDN по-прежнему указывает и container-type: size и inline-size, однако CSS Working Group исключила layout containment из container-type, поэтому он больше не создаёт контекст наложения. Chrome внёс это изменение в версии 129, а Firefox и Safari с тех пор последовали его примеру.

Три типичных случая: симптом, причина, решение

Модальное окно под шапкой

Симптом: оверлей затемняет страницу, но шапка остаётся яркой поверх него. Причина: модальное окно отрисовывается внутри обёртки с transform, opacity или позиционированным z-index, а контекст шапки стоит выше этой обёртки. Решение: перенесите модальное окно в <body> (см. способы исправления ниже).

Выпадающее меню обрезается в карточке с transform

У выпадающего меню внутри карточки с transform может быть одна из двух проблем — наложение или обрезка, поэтому сначала определите, какая именно у вас. Если меню отрисовывается под следующей карточкой, это наложение: карточка с transform является контекстом, и карточки, идущие в DOM позже, рисуются поверх неё. Поднимите открытую карточку:

.card.is-open { position: relative; z-index: 1; }

Если меню обрезается по краю самой карточки, это обрезка. Проверьте overflow у карточки и её предков. Любое значение, кроме visible, обрезает меню, и никакое значение z-index его не вернёт.

Sticky-элемент не остаётся сверху

Симптом: при наведении или прокрутке карточки наезжают на sticky-шапку. Причина: любой элемент с position: fixed или sticky создаёт контекст. Но без z-index он находится на уровне 0, и последующие соседние элементы, создающие контексты (например, карточки с transform при наведении), рисуются поверх него, потому что идут в DOM позже. Решение:

.site-header { position: sticky; top: 0; z-index: var(--z-sticky); }

Если шапка вообще не «прилипает», причина обычно в overflow у одного из предков. Полный пример создания sticky-шапки — в статье Building a responsive sticky navbar with CSS.

Три способа освободить «запертый» элемент

Любое исправление для «запертого» элемента меняет структуру контекстов наложения, а не число в z-index: переместите элемент, перенесите свойство или намеренно установите границу там, где она нужна.

Переместите элемент в DOM. У оверлея, который является прямым потомком <body>, над ним нет контекстов предков, поэтому его z-index сравнивается на корневом уровне. Именно поэтому порталы так хорошо подходят для оверлеев, которые должны перекрывать всю страницу:

const modal = document.querySelector('.modal');

function openModal() {
  document.body.appendChild(modal); // escape every ancestor context
  modal.hidden = false;
}

Удалите или перенесите свойство. Энди Блум (Andy Blum) из Lullabot описал случай, когда в макете с письмом справа налево контейнер с фоновым видео зеркально отражался с помощью transform, из-за чего кнопка воспроизведения/паузы оказывалась под другими элементами. Он решил проблему, перенеся transform с контейнера на iframe с видео:

/* Before: the wrapper becomes a context */
[dir="rtl"] .hero { transform: scaleX(-1); }

/* After: only the media is mirrored */
[dir="rtl"] .hero iframe { transform: scaleX(-1); }

Добавьте изоляцию намеренно. Свойство isolation: isolate на корневом элементе компонента целенаправленно создаёт контекст, и все z-index внутри компонента остаются локальными:

.card { isolation: isolate; }
.card .badge { position: absolute; z-index: 1; } /* never competes with the site header */

Профилактика: шкала значений z-index

z-index, равный 9999, почти всегда означает, что кто-то так и не нашёл контекст наложения, а вовсе не то, что понадобилось большее число. Замените разовые значения небольшой задокументированной шкалой:

:root {
  --z-base: 0;
  --z-dropdown: 100;
  --z-sticky: 200;
  --z-overlay: 300;
  --z-modal: 400;
  --z-toast: 500;
}
.modal { z-index: var(--z-modal); }

Токены шкалы z-index упорядочивают только элементы в общем контексте наложения. Поэтому лучше всего они работают в связке с порталами для слоёв уровня страницы и isolation: isolate на корневых элементах компонентов.

Заключение

Неработающий z-index — почти всегда проблема контекста наложения, а не числа. В следующий раз, когда 9999 проиграет, не трогайте значение. Откройте панель Layers или 3D View, чтобы убедиться, что и где отрисовывается, затем поднимитесь по чек-листу от обоих элементов, пока цепочки не сойдутся. Два потомка общего предка — это и есть сравниваемые элементы, и исправление нужно вносить в один из них.

FAQ

Почему z-index вообще никак не влияет на мой элемент?

z-index применяется только к позиционированным элементам, то есть с position: relative, absolute, fixed или sticky, а также к flex- и grid-элементам. У элемента с позиционированием по умолчанию position: static, который не является flex- или grid-элементом, браузер полностью игнорирует z-index. Добавьте position: relative, чтобы значение начало действовать, а затем проверьте, не ограничивает ли его контекст наложения одного из предков.

Выходит ли диалог, открытый через showModal(), из контекстов наложения?

Да. Вызов showModal() у элемента dialog переносит его в верхний слой (top layer), который находится над всеми контекстами наложения документа. Transform и значения z-index у предков больше не влияют на его порядок. Поповеры, показанные через showPopover(), переносятся туда же. Внутри верхнего слоя z-index не действует: элементы располагаются в порядке переноса, поэтому последний открытый оказывается сверху.

Почему мой оверлей с position: fixed позиционируется относительно родителя, а не области просмотра?

Предок со значением transform, perspective или filter, отличным от none, становится содержащим блоком для потомков с фиксированным позиционированием. Фиксированный оверлей внутри него позиционируется и получает размеры относительно этого предка, а не области просмотра. То же свойство создаёт и контекст наложения, поэтому оверлей может одновременно оказаться под другим контентом и перекрывать не ту область. Перенос оверлея в body решает обе проблемы.

Можно ли с помощью отрицательного z-index поместить потомка за родителя?

Только если родитель не создаёт контекст наложения. Внутри контекста наложения браузер сначала отрисовывает фон и рамки корня контекста, затем потомков с отрицательным z-index, затем контент в потоке. Если у родителя opacity меньше 1, transform или позиционирование с z-index, отличным от auto, потомок с z-index: -1 всё равно отрисуется поверх фона родителя, хотя и под его обычным контентом.

Open-source session replay

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.