26 November 2018

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

Website developmentCSSHTML
Наверное, почти каждый из нас хоть раз в жизни использовал свойство 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.

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

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

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

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

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

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

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

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-ов, и из обычных блоков, порядок при этом сохраняется таким, как был в исходном дереве.

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

<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 у потомка). Надеюсь, данная статья оказалась вам чем-то полезна.
Tags:htmlcssz-index
Hubs: Website development CSS HTML
+16
13.7k 139
Comments 7