Наверное, почти каждый из нас хоть раз в жизни использовал свойство z-index. При этом каждый разработчик уверен, что знает, как оно работает. В самом деле — что может быть проще операций с целыми числами (сравнение и назначение их элементам). Но всё ли так просто, как кажется на первый взгляд?
Возможно, информация, которую я расскажу ниже, на самом деле тривиальна. Однако я уверен, что многие найдут её для себя полезной. Те же, кто уже о ней знал, смогут использовать данный текст как шпаргалку в трудную минуту. Итак, добро пожаловать под кат.
На самом деле человек обычно начинает пытаться разобраться в какой-то новой для себя области в трёх случаях: если встречает неожиданные результаты при работе и не понимает, что происходит; если встаёт потребность выйти за рамки и взглянуть на предмет под другим углом; и наконец, просто из спортивного интереса.
Мой случай явно не относится к третьей категории. Сначала я несколько раз в жизни сталкивался с первым сценарием при работе над разными проектами; однако до конца так и не разбирался в вопросе из-за лени и отсутствия наглядных и понятных материалов с примерами. А затем в начале этого года начал писать веб-движок, что заставило начать читать стандарты и вообще смотреть, как работают разные нетривиальные вещи в популярных браузерах, и главное, почему они работают именно так.
Начнём с простого. Что такое z-index и для чего он нужен?
Очевидно, что это координата по оси Z, задаваемая для некоторого элемента. Ось Z при этом направлена в сторону пользователя. Больше число — ближе элемент.
Почему числа 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>
Похоже, что-то пошло не так. Мы сделали у первого блока z-index больше чем у второго, так почему же он отображается ниже? Да, он идёт по коду раньше — но казалось бы, это должно играть роль только при равных значениях z-index.
На этом месте самое время открыть стандарт CSS2.1, а точнее приложение к нему, касающееся обработки контекстов наложения. Вот ссылка.
Из этого небольшого и очень сжатого текста можно сразу вынести много важной информации.
- z-index управляют наложением не отдельных элементов, а контекстов наложения (групп элементов)
- Мы не можем произвольно управлять элементами в разных контекстах друг относительно друга: здесь работает иерархия. Если мы уже находимся в «низком» контексте, то мы не сможем сделать его элемент выше элемента более «высокого» контекста.
- z-index вообще не имеет смысла для элементов в нормальном потоке (у которых свойство position равно static). В эту ловушку мы и попались в примере выше.
- Чтобы элемент задал новый контекст наложения, он должен быть позиционирован, и у него должен быть назначен z-index.
- Если элемент позиционирован, но z-index не задан, то можно условно считать, что он равен нулю (для простых случаев это работает так, нюансы рассмотрим позже).
- А ещё отдельные контексты наложения задаются элементами со значением opacity, меньшим единицы. Это было сделано для того, чтобы можно было легко перенести альфа-блендинг на последнюю стадию отрисовки для обработки видеокартой.
Но и это ещё не всё. Оказывается, с элементами без z-index тоже не всё так просто, как может показаться.
Процесс отрисовки элементов поддерева контекста можно разбить на несколько стадий (первые две из которых — непосредственно вывод фонового цвета и фонового изображения текущего элемента, задающего контекст).
Итак, рассмотрим весь список.
3. Вывод дочерних контекстов с отрицательными z-index
4. Вывод дочерних блочных элементов в нормальном потоке (только фоны)
5. Вывод дочерних float элементов
6. Вывод контента элементов в нормальном потоке: инлайновые и инлайново-блочные потомки, инлайновый контент внутри блочных потомков, включая строки текста *
7. Вывод дочерних контекстов с нулевыми и auto z-index **
8. Вывод дочерних контекстов с положительными z-index
* в порядке обхода дерева depth-first
** для контекстов с z-index: auto все дочерние контексты считать потомками текущего контекста, то есть «вытаскивать» их наверх на текущий уровень
Уже не так просто, правда? Можно примерно проиллюстрировать данную схему следующей картинкой:
Также есть возможность открыть пример на 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>
Здесь дочерний элемент имеет z-index и позиционирован. Он «всплывает» наверх, но выводится под синим квадратом, поскольку элементы с отрицательными z-index выводятся на стадии 3, а float элементы — на стадии 5.
<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>
В данном примере второй элемент (зелёный) выводится раньше первого (голубого), и поэтому ниже. Однако дочерние элементы вытягиваются наверх (поскольку задают собственные контексты), поэтому в данном случае они идут в том же порядке, в котором они идут именно в исходном дереве (порядок их предков после перестановки не важен!). Если у первого дочернего элемента выставить z-index равный 1, то получим уже такую картинку:
Добавим больше элементов.
<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>
Тут же вообще всё очень интересно. С одной стороны, всё кажется логичным: дочерние контексты вытаскиваются и из 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>
Как видим, «выпрыгнуть» из block элемента — в отличие от остальных случаев вполне возможно, и поскольку у нас всплывает inline-block элемент, он выведется последним по счёту в данном документе.
Как видим, z-index позволяет осуществлять множество интересных трюков (чего стоит хотя бы скрытие элемента под его непосредственным родителем с помощью отрицательного z-index у потомка). Надеюсь, данная статья оказалась вам чем-то полезна.
Автор: popov654