Скриншот редактора кода с большим количеством значений z-index, многие из которых используют !important

Свойство z-index — один из самых важных инструментов в арсенале UI-разработчика, потому что именно оно позволяет управлять порядком наложения элементов на странице. Модальные окна, тосты, попапы, выпадающие списки, тултипы и множество других привычных интерфейсных элементов опираются на него, чтобы гарантированно отображаться поверх остального контента.

Хотя большинство материалов сосредоточены на технических деталях или типичных ловушках Stacking Context (до него мы ещё дойдём…), мне кажется, они упускают один из самых важных и потенциально хаотичных аспектов z-index: само значение.

В большинстве проектов, когда кодовая база дорастает до определённого масштаба, значения z-index превращаются в набор «магических чисел» — в хаотичное поле боя, где каждая команда пытается перебить остальных всё более высокими числами.

С чего вообще началась эта идея

Несколько лет назад я увидел вот такую строчку в одном pull request:

z-index: 10001;

Я тогда подумал: «Ничего себе, какое большое число! Интересно, почему выбрали именно его?» Когда я спросил автора, он ответил: «Ну, я просто хотел убедиться, что элемент будет выше всех остальных на странице, поэтому и взял большое число».

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

Страх оказаться скрытым

Корневая проблема здесь не столько техническая, сколько связанная с недостаточной видимостью общей картины. В большом проекте, где работает несколько команд, ты не всегда знаешь, что ещё сейчас «плавает» на экране. Где-то может быть toast-уведомление от команды A, cookie-баннер от команды B или модалка из маркетингового SDK.

Логика разработчика в таком случае была предельно простой: «Если поставить очень большое число, элемент точно окажется сверху».

Именно так и появляются магические числа — произвольные значения, никак не связанные с остальной частью приложения. Это изолированные догадки, попытка победить в «гонке вооружений» значений z-index.

Мы не будем говорить про Stacking Context… но всё же…

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

Если кратко, элемент с большим значением z-index окажется поверх элемента с меньшим значением только если они находятся в одном и том же Stacking Context.

Если нет, то даже если вы зададите элементу в «нижнем» контексте огромное значение z-index, элементы из «более высокого» стека всё равно останутся над ним — даже если у них самих z-index очень маленький. А значит, иногда элемент может оказаться скрытым, даже если вы выставили ему максимально возможное значение.

А теперь вернёмся к самим значениям.

💡 А вы знали? Максимальное значение z-index2147483647. Почему именно оно? Потому что это максимальное значение 32-битного целого числа со знаком. Если попытаться указать больше, большинство браузеров просто ограничат его этим пределом.

В чём проблема «магических чисел»

Использование произвольных больших значений z-index может привести сразу к нескольким проблемам:

  1. Плохая поддерживаемость: когда вы видите z-index: 10001, это ничего не говорит о его связи с другими элементами. Это просто число, выбранное без какого-либо контекста.
  2. Риск конфликтов: если несколько команд или разработчиков используют большие значения z-index, они легко начинают конфликтовать друг с другом, и в результате одни элементы неожиданно оказываются скрыты за другими.
  3. Сложность отладки: если с порядком наложения что-то пошло не так, понять причину бывает трудно — особенно когда в проекте много элементов с большими значениями z-index. Гораздо лучше работает другой подход.

Я сталкивался с этой «гонкой вооружений» почти в каждом крупном проекте, в котором участвовал. Как только в одном кодбейзе работают несколько команд без стандартизированной системы, хаос рано или поздно берёт верх.

Решение на самом деле очень простое: токенизация значений z-index.

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

Используя токены, вы получаете:

  • Простую и удобную поддержку: все значения хранятся в одном месте.
  • Предотвращение конфликтов: больше не нужно гадать, выше ли 100, чем то, что использует команда B.
  • Более простую отладку: сразу видно, к какому «слою» относится элемент.
  • Более грамотную работу со Stacking Context: такой подход заставляет мыслить слоями системно, а не подбирать случайные числа.

Практический пример

Давайте посмотрим, как это выглядит на практике. Вот простой пример, где слоями управляет централизованный набор токенов в :root:

:root {
  --z-base: 0;
  --z-toast: 100;
  --z-popup: 200;
  --z-overlay: 300;
}

Такая схема невероятно удобна. Если вам нужно добавить новый попап или toast, вы сразу понимаете, какое значение z-index использовать. Если вы захотите поменять порядок — например, поднять тосты выше overlay — вам не придётся искать нужные места по десяткам файлов. Достаточно поменять значения в :root, и всё обновится централизованно.

Что делать, когда появляются новые элементы

Настоящая сила этой системы раскрывается тогда, когда требования меняются. Допустим, вам нужно добавить новый sidebar и разместить его точно между базовым контентом и тостами.

В традиционном подходе вам пришлось бы проверять все существующие элементы и смотреть, какие числа там уже используются. С токенами всё проще: мы просто добавляем новый токен и корректируем шкалу.

:root {
  --z-base: 0;
  --z-sidebar: 100;
  --z-toast: 200;
  --z-popup: 300;
  --z-overlay: 400;
}

При такой настройке не нужно менять ни один существующий компонент. Вы обновляете токены — и всё готово. Логика приложения остаётся последовательной, а вам больше не приходится гадать, какое число «достаточно большое».

Сила относительного позиционирования слоёв

Иногда нам нужно «связать» отдельные слои друг с другом. Отличный пример — фон модального окна или overlay. Вместо того чтобы заводить отдельный токен для фона, можно вычислить его положение относительно основного слоя.

С помощью calc() можно жёстко зафиксировать отношение между элементами, которые всегда должны существовать вместе:

.overlay-background {
  z-index: calc(var(--z-overlay) - 1);
}

Это гарантирует, что фон всегда будет находиться ровно на один шаг ниже overlay, какое бы значение мы ни присвоили токену --z-overlay.

Управление внутренними слоями

До этого момента мы говорили в основном о глобальных слоях приложения. Но что происходит внутри этих слоёв?

Те токены, которые мы завели для основных уровней (100, 200 и т. д.), не подходят для управления внутренними элементами. Дело в том, что большинство таких крупных компонентов создают собственный Stacking Context. Внутри попапа с z-index: 300 значение 301 по сути ничем не отличается от 1. Использовать большие глобальные токены для внутреннего позиционирования — и запутанно, и избыточно.

Примечание: чтобы локальные токены работали предсказуемо, контейнер должен создавать собственный Stacking Context. Если вы работаете с компонентом, у которого его ещё нет (например, у него не задан z-index), можно явно создать его через isolation: isolate.

Чтобы решить эту задачу, можно ввести пару «локальных» токенов специально для внутреннего использования:

:root {
  /* ... глобальные токены ... */
 
  --z-bottom: -10;
  --z-top: 10;
}

Это позволяет точнее управлять внутренним позиционированием. Если вам нужно, чтобы плавающая кнопка действия внутри попапа всегда была сверху, или декоративная иконка в toast находилась за основным содержимым, можно использовать такие локальные опорные значения:

.popup-close-button {
  z-index: var(--z-top);
}
 
.toast-decorative-icon {
  z-index: var(--z-bottom);
}

А если внутренняя раскладка ещё сложнее, вы по-прежнему можете использовать calc() вместе с этими локальными токенами. Например, если внутри компонента несколько элементов накладываются друг на друга, calc(var(--z-top) + 1) (или - 1) даст дополнительную точность, и вам не придётся вообще смотреть на глобальные значения.

Так логика остаётся последовательной: мы думаем о слоях и их позициях системно, а не кидаем случайные числа в надежде, что всё заработает.

Гибкие компоненты: кейс с tooltip

Одна из самых неприятных задач в CSS — управлять компонентами, которые могут появиться где угодно, например tooltip.

Традиционно разработчики задают тултипам огромный z-index вроде 9999, потому что тултип может появиться поверх модального окна. Но если tooltip физически находится внутри DOM-структуры этой модалки, его z-index всё равно относителен только к ней.

На самом деле тултипу просто нужно находиться над тем контентом, к которому он привязан. С локальными токенами мы наконец перестаём гадать:

.tooltip {
  z-index: var(--z-top);
}

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

Отрицательные значения тоже полезны

Отрицательные значения часто пугают разработчиков. Кажется, что элемент с z-index: -1 обязательно исчезнет за фоном страницы или каким-нибудь далёким родителем.

Но в рамках системного подхода отрицательные значения — мощный инструмент для внутренних декоративных элементов. Когда компонент создаёт собственный Stacking Context, его z-index ограничен рамками самого компонента. И z-index: var(--z-bottom) в этом случае просто означает: «помести этот элемент позади основного содержимого именно этого контейнера».

Это отлично подходит для:

  • Фонов компонентов: ненавязчивых паттернов или градиентов, которые не должны мешать тексту.
  • Имитации теней: когда нужен больший контроль, чем даёт box-shadow.
  • Внутренних свечений и рамок: элементов, которые должны находиться «под» основным UI.

Заключение: манифест z-index

Всего с несколькими CSS-переменными мы получили полноценную систему управления z-index. Это простой, но мощный способ сделать так, чтобы работа со слоями больше не превращалась в угадайку.

Чтобы кодовая база оставалась чистой и масштабируемой, вот золотые правила работы с z-index:

  1. Никаких магических чисел: никогда не используйте произвольные значения вроде 999 или 10001. Если число не встроено в систему — это потенциальный баг.
  2. Токены обязательны: каждый z-index в вашем CSS должен приходить из токена — либо глобального токена слоя, либо локального токена позиционирования.
  3. Проблема редко в самом числе: если элемент не оказывается сверху даже с «большим» значением, почти наверняка проблема в его Stacking Context, а не в числе как таковом.
  4. Думайте слоями: перестаньте спрашивать «какое число здесь поставить повыше?» и начните спрашивать «к какому слою относится этот элемент?»
  5. calc() для связей: используйте calc(), чтобы связывать элементы между собой (например, overlay и его фон), а не раздавать им отдельные несвязанные токены.
  6. Локальные контексты для локальных задач: используйте локальные токены (--z-top, --z-bottom) и внутренние stacking context, чтобы управлять сложностью внутри компонентов.

Следуя этим правилам, вы превращаете z-index из хаотичного источника багов в предсказуемую и управляемую часть дизайн-системы. Ценность z-index не в том, насколько большое число вы поставили, а в системе, которая это число определяет.

Бонус: как поддерживать систему в чистоте

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

Чтобы этого не произошло, я сделал библиотеку, специально предназначенную для поддержки именно такой системы: z-index-token-enforcer.

npm install z-index-token-enforcer --save-dev

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

  • Плагин для Stylelint: для обычного CSS/SCSS.
  • Плагин для ESLint: чтобы ловить буквальные значения в CSS-in-JS и inline-стилях React.
  • CLI-сканер: отдельный скрипт, который можно запускать по файлам напрямую или встроить в CI/CD-пайплайны.

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


Теги: css properties, stacking contexts, z-index

Источник