Два элемента и одно число
На экране два элемента. У одного z-index: 9999, у другого числа нет — но второй рисуется поверх первого. Дописываем !important, пробуем дробные значения, экспоненциальную запись, перезагружаем страницу. Результат не меняется. Ошибка не в величине числа, а в том, что сравниваются не те элементы, на которые смотрит разработчик.
Слои на странице упорядочиваются не глобально, а внутри контекстов наложения. Контекст наложения — это область отрисовки, внутри которой числа сравниваются между всеми её участниками, на какой бы глубине разметки те ни лежали, и не сравниваются ни с чем снаружи. Разделяет числа не вложенность сама по себе, а новый контекст, появившийся на пути: обёртка без свойств-создателей из соревнования не выводит. Если два элемента находятся в разных контекстах, сравниваются не они сами, а их ближайшие предки, создавшие эти контексты. Предок с меньшим числом утаскивает за собой всех своих потомков, даже если у тех числа больше. Поэтому элемент с z-index: 9999 может оказаться позади соседа с z-index: 1, если их ближайшие предки, создавшие контексты, имеют числа 1 и 2.
Кто читает z-index, а кто игнорирует
Свойство z-index работает только у элементов, которые участвуют в раскладке как позиционированные или как флекс- и грид-элементы.
Позиционированные элементы — это те, у кого значение position отлично от static: relative, absolute, fixed, sticky. Для них z-index действует по правилам спецификации позиционирования. Позиционированный элемент с z-index: auto контекст наложения не создаёт — кроме fixed и sticky: эти создают его всегда, даже когда числа нет вовсе.
Флекс- и грид-элементы — это прямые потомки флекс- или грид-контейнера (что из них когда выбирать — в разборе раскладок). Для них z-index действует по правилам спецификаций раскладки, и позиционирование им не требуется. Флекс-элемент со значением position: static число читает, но контекст создаёт только если z-index отличен от auto.
Прямой потомок флекс- или грид-контейнера с position: absolute — уже не флекс- и не грид-элемент. Он подпадает под правила позиционирования, а не раскладки. Если у такого элемента z-index: 1, он создаст контекст наложения, но не будет участвовать в раскладке как флекс-элемент.
Контекст наложения создаётся не только числом, но и другими свойствами. Вот свойства, которые ставят ради другого, а контекст наложения получают довеском, — условие создания у каждого своё и указано в строке:
position: fixedиposition: sticky— создают контекст всегда, даже еслиz-index: auto.transform,scale,rotate,translate,filter,backdrop-filter,perspective,clip-path,mask— создают контекст, если значение неnone.opacity— создаёт контекст, если значение меньше1.mix-blend-mode— создаёт контекст, если значение неnormal.isolation: isolate— создаёт контекст всегда.containсо значениямиlayout,paint,strict,content— создают контекст всегда.container-typeсо значениямиsizeилиinline-size— создаёт контекст всегда.will-change, перечисляющее любое из свойств-создателей, — создаёт контекст всегда.content-visibility: auto— включаетlayout-containment, поэтому создаёт контекст наравне сcontain: layout.- Запущенная анимация любого из свойств-создателей — даёт элементу контекст на время проигрывания, а при
animation-fill-mode: forwards— и после него.
z-index: auto и z-index: 0 стоят на одной ступени порядка отрисовки, но z-index: 0 создаёт контекст наложения, а z-index: auto — нет. Если у элемента z-index: 0, его потомки будут упорядочиваться внутри нового контекста, созданного этим элементом. Если у элемента z-index: auto, его потомки будут упорядочиваться внутри контекста ближайшего предка, создавшего контекст.
Как упорядочены слои внутри одного контекста
Порядок отрисовки внутри контекста наложения фиксирован и не зависит от величины чисел. Вот как упорядочены слои:
- Фон и границы самого элемента-контекста.
- Потомки с отрицательным
z-index. - Блочные потомки в потоке.
- Плавающие элементы.
- Строчное содержимое.
- Позиционированные потомки с
z-index: autoили0. - Потомки с положительным
z-index.
Из этого порядка следуют два важных поведения:
- Позиционированный элемент без заданного
z-indexрисуется поверх непозиционированного соседа. Это происходит потому, что позиционированные элементы попадают в шестую группу, а непозиционированные — в третью. - Потомок с отрицательным
z-indexуходит за содержимое родителя, создавшего контекст, но не за его собственные фон и границы. Если родитель контекста не создаёт, такой потомок всплывает в ближайший вышестоящий контекст и может уехать за фон уже того предка и пропасть совсем.
Как диагностировать проблему: подъём по дереву контекстов
Когда элемент не виден поверх соседа, поднимаемся по предкам и ищем создателя контекста. Вот пошаговая инструкция:
- Начинаем с двух элементов сразу: с того, который не виден, и с того, поверх которого он должен лежать. Число элемента соревнуется не с любым числом на странице, а только с числами участников того контекста, в который попал он сам, — контекста ближайшего предка, создавшего контекст. Поэтому первый вопрос не «создаёт ли элемент контекст», а «лежит ли сосед в том же контексте». Свойства-создатели у самого элемента запирают его потомков в новом контексте, но числа ему самому не дают. Место рядом с соседом при этом всё равно может измениться: непозиционированный элемент, получивший контекст прозрачностью, трансформацией, фильтром,
clip-pathили маской, рисуется уже не вместе с блочными потомками в потоке, а там же, где позиционированные соседи без числа, и порядок между ними решает порядок в разметке. Поэтому свойства самого элемента со счетов не сбрасываем; собственное число, отличное отauto, делает и то и другое — запирает потомков в новом контексте и одновременно сортирует сам элемент среди участников того контекста, в который он попал. Если сосед в том же контексте, подъём не нужен: порядок этих двух читается из порядка отрисовки выше — и по числу, и по группе, в которую каждый попал. - Если сосед лежит в другом контексте, сравнивать числа этих двух бессмысленно: поднимаемся к родителю того элемента, который не виден, и проверяем, создаёт ли он контекст. Если создаёт, сравниваем его число с числом ближайшего предка соседа, создавшего контекст.
- Если родитель контекста не создаёт, повторяем шаг 2 для следующего предка. Продолжаем подъём, пока не найдём создателя контекста или не дойдём до корня документа.
На каждом шаге держим в уме список свойств-создателей. Если у предка есть любое из них, он создаёт контекст, и его число участвует в сравнении. Если у предка нет ни одного свойства-создателя и z-index: auto, контекста он не создаёт — его потомки упорядочиваются внутри контекста ближайшего предка, создавшего контекст, и подъём продолжается дальше вверх. Если же предок контекст создал — хотя бы свойством-создателем при z-index: auto, — его потомки заперты внутри этого контекста, и подъём на нём заканчивается.
Не всегда дело в слоях
Элемент не виден поверх соседа — но это не всегда слои. Вот три разных симптома, которые лечатся по-разному:
- Элемент виден целиком, но позади соседа. Это слои. Лечится переносом узла или созданием контекста у корня компонента.
- Элемент виден кусок, обрезанный по краю предка. Это
overflow: hiddenи цепочка содержащих блоков. Контекста наложенияoverflowне создаёт, но обрезает абсолютно спозиционированного потомка, если входит в цепочку его содержащих блоков. - Элемент виден, но не нажимается. Сверху лежит прозрачный оверлей. Ответ там
pointer-events, а не число. Порядок слоёв — это и порядок попадания курсора. Если элемент оказывается под соседом, он перестаёт получать клики. Прозрачный оверлей с большим числом собирает клики, не будучи видимым. Этот симптом уходит в доступность интерфейса: там разбирается, почему интерактивный элемент оказывается недостижимым.
Четыре правки и цена каждой
Когда число не помогает, пробуем другие способы решения проблемы:
- Перенести узел порталом. Элемент поднимется в дереве документа, но стили, привязанные к вложенности селекторами, за узлом не переедут. Портал меняет положение элемента в DOM, но не в каскаде. Это значит, что если у элемента были стили, зависящие от вложенности, например
.parent .child { color: red }, они перестанут применяться. - Создать контекст осознанно у корня компонента через
isolation: isolate. Цена — изолированная группа смешивания у потомков.mix-blend-modeу потомков перестанет смешиваться с тем, что нарисовано позади элемента-контекста. Это может сломать визуальные эффекты, если они были завязаны на смешивании. - Убрать свойство у предка. Цена зависит от того, какое это свойство: контекст наложения создаёт длинный список, а содержащим блоком предка делает только часть из этого списка. Если предок держал контекст трансформацией, фильтром,
perspective, containment илиwill-changeс любым из них, снятие починит слои и тем же движением сдвинет потомков сfixedиabsolute: отсчёт у них пойдёт от другого предка. Если контекст держался прозрачностью,mix-blend-modeилиisolation: isolate, геометрия не двинется. Но в обоих случаях уходит и то, ради чего свойство ставили, — поэтому правка годится там, где свойство осталось от прежней задачи, а не держит текущую. - Уйти в верхний слой. Он действительно вне соревнования чисел, но попасть туда нужно правильным способом: диалог — только при модальном открытии, всплывающий элемент — при показе как popover; открытый обычным способом диалог остаётся в общих правилах слоёв, и цена ошибки здесь в том, что правка выглядит сделанной, а слои не изменились. Внутри верхнего слоя есть свой порядок, который числом не переставляется, — подробности в современном CSS.
Каскадные слои @layer — это не слои отрисовки: они решают, какое объявление победит в каскаде, а не что окажется сверху на экране.
Тупик: почему растут числа, а результат не меняется
Первым делом разработчики увеличивают число. Потом дописывают !important. Потом пробуют дробные значения или экспоненциальную запись — и получают не «слабее», а «никак»: z-index принимает только целое, а дробное и экспоненциальное объявление невалидно и отбрасывается целиком, как будто его не писали. Но и целое не помогает: сравнение идёт только между соседями внутри одного контекста наложения. Поддерево предка, создавшего контекст, целиком лежит в том слое, который занял сам предок.
Побочный эффект тупика — гонка чисел. В проекте заводятся числа 999, 9999, 99999, и каждое следующее должно быть больше предыдущего, чтобы перекрыть его. Это приводит к тому, что числа теряют смысл, а код становится сложнее поддерживать.
Позиция редакции
z-index — не глобальная шкала глубины на весь документ, а локальная сортировка соседей внутри одного контекста наложения. Правильная правка состоит не в увеличении числа, а в переносе узла или в осознанном создании контекста у корня компонента.
Числа, вынесенные наружу компонента, редакция считает дефектом, даже когда они работают. Проверяемая форма правила — контекст у корня компонента, короткая локальная шкала внутри, наружу ни одного числа. Это делает код предсказуемее и проще в поддержке.
Неверна эта позиция, если оба элемента — прямые соседи внутри одного контекста и ни один предок контекста не создаёт. Тогда порядок действительно решается числом или порядком в разметке, и увеличить z-index — правильный и достаточный ответ. В этом случае число работает как локальная сортировка, и его увеличение решает проблему без побочных эффектов.