Что такое «Ад z-index» и когда он появляется?

Обычно «адом z-index» в фронтенде называют ситуацию, когда на проекте хаотично и без системы раскиданы значения z-index, из‑за чего всё начинает перекрывать всё, а поведение слоёв становится непредсказуемым и плохо управляемым. Это не формальный термин из спецификации, а жаргон, которым разработчики описывают накопившийся бардак с контекстами наложения и слоями интерфейса.

Что именно имеют в виду

  • На странице десятки или сотни элементов с z-index: 1, 10, 99, 9999, 2147483647 и т.п., выставленных «на глаз», без единой шкалы и правил.
  • Элементы внезапно перестают перекрывать друг друга так, как ожидается, из‑за контекстов наложения (position, opacity, transform и т.д.), и для «починки» начинают ещё сильнее поднимать z-index.
  • В итоге любой новый попап, модалка или тултип требуют «подбор значения, которое перебьёт всё остальное», а изменить верстку, не сломав старые слои, становится почти нереально.

Как это выглядит на практике

Типичные признаки «ада z-index»:

  • У модалок и оверлеев огромные значения z-index просто потому, что «иначе не работает».
  • В разных компонентах используются свои независимые шкалы слоёв, не согласованные между собой.
  • Любая правка layout’а может внезапно «утопить» важные элементы под другими слоями, и отладка превращается в охоту за невидимыми контекстами наложения.

Как от этого обычно спасаются

  • Вводят общую шкалу слоёв: например, базовые элементы, хедер, сайдбар, попапы, модалки, нотификации — каждый в своём диапазоне.
  • Описывают эти уровни в токенах/переменных (SASS, CSS custom properties, дизайн‑система), вместо «магических чисел» по месту.
  • Минимизируют создание новых контекстов наложения и аккуратно используют свойства, которые их создают (position, opacity < 1, transform, filter, etc.).

По сути, «ад z-index» — это не про само свойство, а про отсутствие архитектуры слоёв и дисциплины в верстке: когда z-index превращается из простого инструмента в источник бесконечных костылей.

Какие свойства влияют на создание контекста z index

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

Базовые случаи

  • Корневой элемент документа <html> всегда создаёт корневой контекст наложения
  • Любой элемент с позиционированием position не static и с z-index, отличным от auto, создаёт новый контекст наложения (relativeabsolutefixedsticky + z-index: 0/1/...)​

Свойства, создающие новый контекст

Новый stacking context создают, даже без z-index:

  • opacity < 1 (любое значение строго меньше 1)
  • Любое ненулевое transform (transform: translate(...)scale(...)rotate(...) и т.п.)
  • Любой filter (blurdrop-shadowbrightness и т.д.)​
  • will-change, если в нём указаны свойства, создающие композитный слой (transformopacity и др.)​
  • mix-blend-modeisolation: isolateclip-pathmask/mask-imageperspective тоже могут порождать собственный контекст наложения

Дополнительные случаи

  • Элементы со position: fixed и position: sticky обычно ведут себя как корни собственных контекстов наложения​
  • Некоторые свойства в сочетании (например, transform + position или filter + opacity) гарантированно создают отдельный стек, из-за чего внутренний z-index уже не может «перебить» элементы снаружи​

Итоговая памятка

Чаще всего контекст наложения появляется, когда:

  • Есть непустой z-index у непозиционированного по умолчанию элемента.
  • Включены эффекты, которые заставляют браузер делать отдельный «слой отрисовки»: opacity < 1transformfilterwill-changeclip-path и похожие.

Краткая шпаргалка по контекстам наложения (z-index)

Основные случаи

Условие / свойствоСоздаёт контекст наложения?Комментарий / типичные грабли
html (корень документа)ДаВсегда корневой контекст, всё внутри него.
positionstatic + z-index != autoДаКлассика: relative/absolute/fixed/sticky + z-index: 0/1/.... Внутренний z-index уже не перебивает внешние элементы.​

Часто встречающиеся свойства

Свойство / условиеСоздаёт контекст?Что важно помнить
opacity < 1ДаЛюбое значение строго меньше 1. Модалки/оверлеи с полупрозрачностью могут «запереть» детей внутри своего контекста.​
Любой transformДаtranslate/scale/rotate/... создают новый контекст. Частая причина, почему элемент «не может» перекрыть фиксированный хедер.​
Любой filterДаЛюбой фильтр (blur, drop-shadow, brightness и т.п.) делает свой локальный стек.​
will-change: transform/opacity/...ДаПодготовка к анимации создаёт отдельный слой, даже если эффект ещё не применён.​
mix-blend-mode != normalДаЭлемент смешивает слои и одновременно становится корнем собственного контекста.​
isolation: isolateДаПринудительно создаёт новый контекст для элемента. Удобно, чтобы «обрубить» влияние внешних слоёв.​
clip-pathДаЛюбая обтравка тоже отрывает элемент в отдельный контекст.​
Маски (mask, mask-image)ДаАналогично clip-path, всё внутри живёт в своём стеке.​
perspective на элементеДаЕсли не на transform-style: preserve-3d предке, может создать новый контекст для потомков.​

Позиционирование и специальные случаи

Свойство / ситуацияСоздаёт контекст?Комментарий
position: fixedЧасто даВ большинстве реализаций считается корнем собственного контекста; ведёт себя поверх обычного потока, но может быть «заперт» transform-родителем.​
position: stickyЧасто даВедёт себя как корень контекста в зоне прилипания; взаимодействует с родительскими контекстами.​
Flex-контейнер (display: flex)Нет сам по себеНо его дети с z-index и/или position участвуют в своём порядке наложения; доп. свойства могут создать контекст.​
Grid-контейнер (display: grid)Нет сам по себеАналогично flex: контекст появится при свойствах из таблиц выше.​

Если что‑то «не перекрывается как надо», почти всегда виноват новый контекст наложения. Ниже — короткий чеклист.

Когда попап / тултип не перекрывает

  1. Проверь родителя на transform/opacity/filter
  • Если у любого предка стоит transformopacity < 1filterwill-changeclip-pathmix-blend-modeisolation: isolate — он создаёт новый контекст, и z-index ребёнка не может перебить элементы снаружи.​
  • Лечение: убрать эффект с предка, поднять наверх DOM‑дерева, либо повесить попап ближе к body (портал).​
  1. Убедись, что z-index задан правильно
  • У элемента с z-index должен быть position не static; иначе значение игнорируется.​
  • Если у родителя z-index: auto, но при этом на нём создан контекст (например, opacity < 1), то его дети не «пробьют» соседний контекст вообще, как бы ты ни крутил их z-index.​

Когда шапка/оверлей «прячут» всё остальное

  1. Проверь, не слишком ли рано создан «верхний» контекст
  • Частая ошибка: на самом верхнем контейнере (.app.wrapper) висят transform или filter (из-за анимаций, скролла и т.п.), и весь интерфейс оказывается в одном внутреннем контексте, который ниже фиксированной шапки/оверлея.​
  • Лечение: вынести «глобальные» слои (хедер, модалки, тосты) за пределы этого контейнера или убрать с него свойства, создающие контекст.​
  1. Не завышай бессистемно z-index
  • Если приходится докручивать до 9999 и выше — проблемма не в числе, а в структуре контекстов.​
  • Введи шкалу уровней (например: базовый 0–10, шапка 100, сайдбар 200, попапы 1000, модалки 2000, алерты 3000) и зафиксируй её в переменных/токенах.​

Ещё пару правил на каждый день

  1. Минимизируй создание новых контекстов
  • Перед тем как повесить transform/filter/opacity на крупный контейнер (layout, карточки, списки) — подумай, не нужно ли будет потом показывать поверх него попапы/хинты.​
  • Часто лучше анимировать внутренний элемент, а не весь контейнер, чтобы не ломать стек наложения вокруг.​
  1. Отладка по шагам в DevTools
  • Включай инспектор, смотри computed‑стили: есть ли transformopacity < 1filterposition + z-index у предков.​
  • Если элемент «проигрывает» в наложении, сравни контексты: кто выше по дереву, у кого собственный стек, и на каком уровне реально сравниваются z-index.​