Что такое…

Что такое z-index — и почему 9999 не помогает

Иллюстрация: две стопки стеклянных пластин, яркая пластина заперта внутри нижней

Знакомая сцена: выпадающее меню прячется под шапкой. Ты ставишь ему z-index: 100. Не помогло. Ставишь 9999. Тоже. Ставишь 999999 — и с тем же результатом.

Вот в чём фокус: твой 9999 вообще не спорил с шапкой. Он проиграл ещё до начала спора, потому что какой-то родитель выше по дереву случайно создал контекст наложения — и утащил твоё меню целиком вниз вместе с собой. Разберёмся, как это работает.

z-index — это не «высота», это место в очереди

По умолчанию браузер рисует элементы в порядке появления в HTML: кто ниже в коде, тот сверху на экране. z-index позволяет вмешаться в этот порядок вручную — задать номер в очереди отрисовки.

Две вещи, которые ломают ожидания сразу:

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

Почему 9999 проигрывает единице

Представь стопки прозрачных пластин на столе. Внутри стопки ты можешь двигать пластину вверх и вниз сколько угодно. Но если вся твоя стопка лежит под соседней — верхняя пластина в ней всё равно останется внизу. Она поднялась на самый верх своей стопки, а стопки уже между собой договорились.

Такая стопка в CSS называется контекстом наложения. Правило одно:

z-index сравнивается только между соседями внутри одного контекста наложения. Снаружи весь контекст выступает одним элементом.

То есть если родитель твоей модалки лежит под шапкой, никакое число внутри не поможет. Спор идёт не на твоём уровне.

Что создаёт контекст наложения (список, который стоит знать)

Самое коварное: контекст создаёт куча свойств, которые ставят совсем для другого. Вот основные, по документации MDN:

  • position: relative или absolute вместе с z-index, отличным от auto;
  • position: fixed или sticky — всегда, даже без z-index;
  • opacity меньше 1 — да, opacity: 0.99 тоже;
  • transform, filter, backdrop-filter с любым не-начальным значением;
  • will-change со свойством, которое само создало бы контекст;
  • contain: layout, contain: paint, contain: strict, contain: content;
  • container-type: size или inline-size — да, включённые контейнерные запросы тоже создают контекст;
  • isolation: isolate — единственный пункт, который для этого и придуман.

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

Как чинить по-настоящему

Шаг 1. Найди виновного родителя. Открой инструменты разработчика, выбери свой элемент и иди вверх по цепочке родителей, высматривая transform, opacity, filter, position: fixed. В Chrome есть прямая подсказка: на вкладке Layers видно, кто во что вложен. Нашёл — дальше есть выбор.

Шаг 2. Подними не элемент, а контекст. Если модалка заперта в карточке — z-index нужно поднимать карточке, а не модалке. Часто этого достаточно.

Шаг 3. Или вынеси элемент из проблемного родителя. Модалки и подсказки почти всегда лучше рендерить в конце body, а не там, где они логически «принадлежат». В React для этого есть порталы, они делают ровно это — переносят узел в другое место DOM.

Шаг 4. Наведи порядок заранее. Свойство isolation: isolate создаёт контекст намеренно: внутренние z-index перестают влиять на остальную страницу. Удобно вешать на карточку или виджет, чтобы чужие 9999 не пролезали к тебе. Работает во всех браузерах с 2014–2015 годов.

Способ вообще не участвовать в споре

Если элемент по смыслу — модальное окно или всплывашка, у браузера есть отдельный этаж: top layer. Это слой над всей страницей, и попадает в него не тот, у кого больше число, а тот, кого туда положил сам браузер.

Туда попадают: элемент, открытый через showModal() у dialog, элемент с включённым popover, и полноэкранный элемент через Fullscreen API.

Практический вывод: вместо div с position: fixed и z-index: 9999 возьми настоящий dialog и открывай его методом showModal(). Он будет поверх всего независимо от того, сколько контекстов наложения ты понаделал — плюс бесплатно получишь закрытие по Esc и правильное поведение фокуса.

Коротко: не поднимай число — найди стопку.

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

Потому что по стандарту z-index применяется к позиционированным элементам. Добавь position: relative — визуально ничего не сдвинется, но z-index начнёт действовать. Исключение — прямые дети flex- и grid-контейнеров: там он работает и так.

Можно ли ставить отрицательный z-index?

Можно: элемент уйдёт за фон своего родителя, но останется внутри своего контекста наложения. Частый приём для декоративных подложек. За пределы контекста он всё равно не уедет.

Какие числа выбирать?

Любые осмысленные и редкие: например, 1, 10, 100 для слоёв интерфейса. Гонка «9999 против 10000» — признак того, что проблема в контекстах, а не в числах. Если страница ведёт себя странно и без модалок, загляни в почему не работает CSS — там про соседнюю пару причин.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →