Webrium
Подписаться

z-index растёт, а элемент всё равно внизу

Как работают контексты наложения и почему z-index иногда игнорирует даже огромные числа

Вложенный элемент с z-index 9999 оказывается ниже из-за контекста предка с z-index 19999 внутри 1

Два элемента и одно число

На экране два элемента. У одного 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, его потомки будут упорядочиваться внутри контекста ближайшего предка, создавшего контекст.

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

Порядок отрисовки внутри контекста наложения фиксирован и не зависит от величины чисел. Вот как упорядочены слои:

  1. Фон и границы самого элемента-контекста.
  2. Потомки с отрицательным z-index.
  3. Блочные потомки в потоке.
  4. Плавающие элементы.
  5. Строчное содержимое.
  6. Позиционированные потомки с z-index: auto или 0.
  7. Потомки с положительным z-index.

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

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

Как диагностировать проблему: подъём по дереву контекстов

Когда элемент не виден поверх соседа, поднимаемся по предкам и ищем создателя контекста. Вот пошаговая инструкция:

  1. Начинаем с двух элементов сразу: с того, который не виден, и с того, поверх которого он должен лежать. Число элемента соревнуется не с любым числом на странице, а только с числами участников того контекста, в который попал он сам, — контекста ближайшего предка, создавшего контекст. Поэтому первый вопрос не «создаёт ли элемент контекст», а «лежит ли сосед в том же контексте». Свойства-создатели у самого элемента запирают его потомков в новом контексте, но числа ему самому не дают. Место рядом с соседом при этом всё равно может измениться: непозиционированный элемент, получивший контекст прозрачностью, трансформацией, фильтром, clip-path или маской, рисуется уже не вместе с блочными потомками в потоке, а там же, где позиционированные соседи без числа, и порядок между ними решает порядок в разметке. Поэтому свойства самого элемента со счетов не сбрасываем; собственное число, отличное от auto, делает и то и другое — запирает потомков в новом контексте и одновременно сортирует сам элемент среди участников того контекста, в который он попал. Если сосед в том же контексте, подъём не нужен: порядок этих двух читается из порядка отрисовки выше — и по числу, и по группе, в которую каждый попал.
  2. Если сосед лежит в другом контексте, сравнивать числа этих двух бессмысленно: поднимаемся к родителю того элемента, который не виден, и проверяем, создаёт ли он контекст. Если создаёт, сравниваем его число с числом ближайшего предка соседа, создавшего контекст.
  3. Если родитель контекста не создаёт, повторяем шаг 2 для следующего предка. Продолжаем подъём, пока не найдём создателя контекста или не дойдём до корня документа.

На каждом шаге держим в уме список свойств-создателей. Если у предка есть любое из них, он создаёт контекст, и его число участвует в сравнении. Если у предка нет ни одного свойства-создателя и z-index: auto, контекста он не создаёт — его потомки упорядочиваются внутри контекста ближайшего предка, создавшего контекст, и подъём продолжается дальше вверх. Если же предок контекст создал — хотя бы свойством-создателем при z-index: auto, — его потомки заперты внутри этого контекста, и подъём на нём заканчивается.

Не всегда дело в слоях

Элемент не виден поверх соседа — но это не всегда слои. Вот три разных симптома, которые лечатся по-разному:

  1. Элемент виден целиком, но позади соседа. Это слои. Лечится переносом узла или созданием контекста у корня компонента.
  2. Элемент виден кусок, обрезанный по краю предка. Это overflow: hidden и цепочка содержащих блоков. Контекста наложения overflow не создаёт, но обрезает абсолютно спозиционированного потомка, если входит в цепочку его содержащих блоков.
  3. Элемент виден, но не нажимается. Сверху лежит прозрачный оверлей. Ответ там pointer-events, а не число. Порядок слоёв — это и порядок попадания курсора. Если элемент оказывается под соседом, он перестаёт получать клики. Прозрачный оверлей с большим числом собирает клики, не будучи видимым. Этот симптом уходит в доступность интерфейса: там разбирается, почему интерактивный элемент оказывается недостижимым.

Четыре правки и цена каждой

Когда число не помогает, пробуем другие способы решения проблемы:

  1. Перенести узел порталом. Элемент поднимется в дереве документа, но стили, привязанные к вложенности селекторами, за узлом не переедут. Портал меняет положение элемента в DOM, но не в каскаде. Это значит, что если у элемента были стили, зависящие от вложенности, например .parent .child { color: red }, они перестанут применяться.
  2. Создать контекст осознанно у корня компонента через isolation: isolate. Цена — изолированная группа смешивания у потомков. mix-blend-mode у потомков перестанет смешиваться с тем, что нарисовано позади элемента-контекста. Это может сломать визуальные эффекты, если они были завязаны на смешивании.
  3. Убрать свойство у предка. Цена зависит от того, какое это свойство: контекст наложения создаёт длинный список, а содержащим блоком предка делает только часть из этого списка. Если предок держал контекст трансформацией, фильтром, perspective, containment или will-change с любым из них, снятие починит слои и тем же движением сдвинет потомков с fixed и absolute: отсчёт у них пойдёт от другого предка. Если контекст держался прозрачностью, mix-blend-mode или isolation: isolate, геометрия не двинется. Но в обоих случаях уходит и то, ради чего свойство ставили, — поэтому правка годится там, где свойство осталось от прежней задачи, а не держит текущую.
  4. Уйти в верхний слой. Он действительно вне соревнования чисел, но попасть туда нужно правильным способом: диалог — только при модальном открытии, всплывающий элемент — при показе как popover; открытый обычным способом диалог остаётся в общих правилах слоёв, и цена ошибки здесь в том, что правка выглядит сделанной, а слои не изменились. Внутри верхнего слоя есть свой порядок, который числом не переставляется, — подробности в современном CSS.

Каскадные слои @layer — это не слои отрисовки: они решают, какое объявление победит в каскаде, а не что окажется сверху на экране.

Тупик: почему растут числа, а результат не меняется

Первым делом разработчики увеличивают число. Потом дописывают !important. Потом пробуют дробные значения или экспоненциальную запись — и получают не «слабее», а «никак»: z-index принимает только целое, а дробное и экспоненциальное объявление невалидно и отбрасывается целиком, как будто его не писали. Но и целое не помогает: сравнение идёт только между соседями внутри одного контекста наложения. Поддерево предка, создавшего контекст, целиком лежит в том слое, который занял сам предок.

Побочный эффект тупика — гонка чисел. В проекте заводятся числа 999, 9999, 99999, и каждое следующее должно быть больше предыдущего, чтобы перекрыть его. Это приводит к тому, что числа теряют смысл, а код становится сложнее поддерживать.

Позиция редакции

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

Числа, вынесенные наружу компонента, редакция считает дефектом, даже когда они работают. Проверяемая форма правила — контекст у корня компонента, короткая локальная шкала внутри, наружу ни одного числа. Это делает код предсказуемее и проще в поддержке.

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

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Два стыка: CSS встроен в конвейер рендеринга и совпадает с кадром, JS-наблюдатель стоит снаружи и разошёлся — опаздывает на кадрCSS в конвейереJS опаздывает
Углубление

Что современный CSS убрал из вашего JS

CSS решает задачи наблюдения за стилями и компоновкой быстрее скриптов — без задержки на кадр и лишних перерасчётов.

Из нескольких CSS-правил применяется только одно, остальные отфильтрованы каскадомПравила CSSПрименено
Углубление

Стиль не применяется: кто победил в каскаде

Как CSS-каскад решает, какие стили применить — и почему z-index или другие свойства могут игнорироваться даже при правильном объявлении.

Сравнение одномерной и двумерной раскладки: Flexbox и Grid как разделённые части одного поляFlexbox (одна ось)Grid (две оси)
Усиление

Flexbox или Grid: как выбрать раскладку

Как правильно сочетать Flexbox и Grid, чтобы избежать конфликтов z-index и других частых поломок вёрстки.

Цепочка шагов аудита доступности: автоматические проверки разметки лежат внутри границы, а последний шаг — поведение по Tab с реальными барьерами — выходит за евидит автоматбарьер в поведении
Практика

Доступность как инженерная задача, а не как галочка

Как проверить доступность интерфейса без инструментов — просто пройдите весь путь клавиатурой и найдите барьеры там, где их не ждали.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

↑↓ выбрать · Enter открыть · Esc закрыть