Хабрахабр

Как на самом деле работают z-index

Наверное, почти каждый из нас хоть раз в жизни использовал свойство z-index. При этом каждый разработчик уверен, что знает, как оно работает. В самом деле — что может быть проще операций с целыми числами (сравнение и назначение их элементам). Но всё ли так просто, как кажется на первый взгляд?

Однако я уверен, что многие найдут её для себя полезной. Возможно, информация, которую я расскажу ниже, на самом деле тривиальна. Итак, добро пожаловать под кат. Те же, кто уже о ней знал, смогут использовать данный текст как шпаргалку в трудную минуту.

image

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

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

Что такое z-index и для чего он нужен? Начнём с простого.

Ось Z при этом направлена в сторону пользователя. Очевидно, что это координата по оси Z, задаваемая для некоторого элемента. Больше число — ближе элемент.

image

Всё просто. Почему числа z-index целые? Поскольку реальный монитор не имеет третьего измерения (мы можем его лишь имитировать), нам нужна некоторая безразмерная величина, единственная задача которой — обеспечивать сравнение элементов (то есть упорядоченность множества). Диапазон практически не ограничен сверху и снизу, поэтому нам нет нужды использовать дробные значения. Целые числа прекрасно справляются с этой задачей, при этом они нагляднее вещественных.

Однако, не всё так просто. Казалось бы, этих знаний достаточно, чтобы начать использовать z-index на страницах.

<div style="background: #b3ecf9; z-index: 1"></div>
<div style="background: #b3ecb3; margin-top: -86px; margin-left: 38px; z-index: 0"></div>

image

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

1, а точнее приложение к нему, касающееся обработки контекстов наложения. На этом месте самое время открыть стандарт CSS2. Вот ссылка.

Из этого небольшого и очень сжатого текста можно сразу вынести много важной информации.

  1. z-index управляют наложением не отдельных элементов, а контекстов наложения (групп элементов)
  2. Мы не можем произвольно управлять элементами в разных контекстах друг относительно друга: здесь работает иерархия. Если мы уже находимся в «низком» контексте, то мы не сможем сделать его элемент выше элемента более «высокого» контекста.
  3. z-index вообще не имеет смысла для элементов в нормальном потоке (у которых свойство position равно static). В эту ловушку мы и попались в примере выше.
  4. Чтобы элемент задал новый контекст наложения, он должен быть позиционирован, и у него должен быть назначен z-index.
  5. Если элемент позиционирован, но z-index не задан, то можно условно считать, что он равен нулю (для простых случаев это работает так, нюансы рассмотрим позже).
  6. А ещё отдельные контексты наложения задаются элементами со значением opacity, меньшим единицы. Это было сделано для того, чтобы можно было легко перенести альфа-блендинг на последнюю стадию отрисовки для обработки видеокартой.

Но и это ещё не всё. Оказывается, с элементами без z-index тоже не всё так просто, как может показаться.

Процесс отрисовки элементов поддерева контекста можно разбить на несколько стадий (первые две из которых — непосредственно вывод фонового цвета и фонового изображения текущего элемента, задающего контекст).

Итак, рассмотрим весь список.

Вывод дочерних контекстов с отрицательными z-index
4. 3. Вывод дочерних float элементов
6. Вывод дочерних блочных элементов в нормальном потоке (только фоны)
5. Вывод дочерних контекстов с нулевыми и auto z-index **
8. Вывод контента элементов в нормальном потоке: инлайновые и инлайново-блочные потомки, инлайновый контент внутри блочных потомков, включая строки текста *
7. Вывод дочерних контекстов с положительными z-index

* в порядке обхода дерева depth-first
** для контекстов с z-index: auto все дочерние контексты считать потомками текущего контекста, то есть «вытаскивать» их наверх на текущий уровень

Можно примерно проиллюстрировать данную схему следующей картинкой: Уже не так просто, правда?

image

Также есть возможность открыть пример на codepen и поиграться с ним своими руками.

Казалось бы, алгоритм и так достаточно сложен: нам нужно сперва подтянуть дочерние контексты внутри псевдоконтекстов (помните про значение auto?), затем произвести сортировку для двух списков z-index, выстроив их в числовой ряд, потом пройти по дочерним элементам: сначала по блочным в нормальном потоке, потом по плавающим, затем по инлайновым и инлайново-блочным… Но и это ещё не всё.

Первый, если повезёт, нас не коснётся. Но тут нас ждёт целых два сюрприза. Связан он с тем, что фон с рамками и содержимое блочных элементов выводятся на разных стадиях — но если наш самописный движок для каждого текстового узла создаёт автоматически инлайновый элемент, то с этим всё будет ок, они естественным образом будут выведены позже.

Заключается он в пометке
А вот второй совсем не так тривиален.

For each one of these, treat the element as if it created a new stacking context, but any positioned descendants and descendants which actually create a new stacking context should be considered part of the parent stacking context, not this new one.

у float и inline-block/inline (но не block!) элементов.

А означает это то, что их мы должны обработать так же, как и элементы с z-index: auto. Что же это означает на практике? Но в остальном мы должны обращаться с ними как с элементами, задающими свой контекст. То есть во-первых, обойти их поддеревья и вытащить оттуда дочерние контексты, поместив их на текущий уровень. И если для дочерних контекстов — это интуитивно ясно (потому что алгоритм рекурсивный), то вот здесь — уже не настолько. Это означает, что всё поддерево внутри них, вытянувшееся после обхода в линейный список, должно остаться атомарным. Или, иными словами, мы не можем перетасовывать порядок элементов так, чтобы потомки такого элемента «всплыли» выше своего родителя.

Поэтому приходится при написании кода движка идти на хитрость с тем, чтобы элементы float, inline и inline-block до до поры не раскрывали своих потомков (за исключением дочерних элементов с позиционированием и z-index, формирующих контексты наложения), а потом запускать для них всю функцию рекурсивно, но уже наоборот с учётом того факта, что дочерние контексты должны при обходе пропускаться.

Несколько примеров для демонстрации этого явления:

<div style="float: left; background: #b3ecf9;"> <div style="width: 40px; height: 40px; background: #fff700; position: relative; z-index: -1; top: -20px; left: -20px;"></div>
</div>

image

Он «всплывает» наверх, но выводится под синим квадратом, поскольку элементы с отрицательными z-index выводятся на стадии 3, а float элементы — на стадии 5. Здесь дочерний элемент имеет z-index и позиционирован.

<div style="float: left; margin-top: -30px; background: #b3ecf9;"> <div style="width: 40px; height: 40px; background: #fff700; position: relative; z-index: 0;"></div>
</div>
<div style="background: #b3ecb3; margin-top: 52px; margin-left: 38px;"> <div style="width: 40px; height: 40px; background: #ff0000; position: relative; z-index: 0;"></div>
</div>

image

Однако дочерние элементы вытягиваются наверх (поскольку задают собственные контексты), поэтому в данном случае они идут в том же порядке, в котором они идут именно в исходном дереве (порядок их предков после перестановки не важен!). В данном примере второй элемент (зелёный) выводится раньше первого (голубого), и поэтому ниже. Если у первого дочернего элемента выставить z-index равный 1, то получим уже такую картинку:

image

Добавим больше элементов.

<div style="float: left; background: #b3ecf9;"> <div style="float: left"> <div style="width: 40px; height: 40px; background: #fff700; position: relative; z-index: 0;"></div> </div>
</div>
<div style=" background: #b3ecb3; margin-top: 32px; margin-left: 40px;"> <div style="position: relative"> <div style="width: 40px; height: 40px; background: #ff0000; position: relative; z-index: 0;"></div> </div>
</div>

image

С одной стороны, всё кажется логичным: дочерние контексты вытаскиваются и из float-ов, и из обычных блоков, порядок при этом сохраняется таким, как был в исходном дереве. Тут же вообще всё очень интересно. Я точно затрудняюсь сказать, почему так, но видимо как раз в силу того, что фон и содержимое блочных элементов выводятся не на одной стадии, и данные элементы как раз проходят как содержимое. Интересно другое: если убрать position: relative у красного квадрата и его обёртки, абсолютно ничего не изменится (хотя по идее это должно приводить к тому, что данные элементы уйдут под жёлтый квадрат).

Не очень понятно, каким образом влияет этот момент, т.к. Ещё более интересный момент состоит в том, что если добавить position: relative к <body>, то произойдёт как раз то, что ожидалось: красный квадрат скроется под жёлтым. Если кто-то знает природу этого явления — напишите в комментариях. у <body> и так по умолчанию создаётся корневой контекст наложения.

Наконец, последний пример:

<div style="background: #b3ecf9;"> <div style="display: inline-block; width: 40px; height: 40px; background: #fc0;"></div>
</div>
<div style="background: #b3ecb3; margin-top: -100px; margin-left: 22px;"></div>

image

Как видим, «выпрыгнуть» из block элемента — в отличие от остальных случаев вполне возможно, и поскольку у нас всплывает inline-block элемент, он выведется последним по счёту в данном документе.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть