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

Комментарии (7)


  1. Aingis
    26.11.2018 12:20

    Ужасные объяснения, которые ничего не объясняют. Даже зная что имеется в виду, я ничего не понимаю.

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

    <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

    У вас стили не соответствуют картинке. Если привести размеры, то получается такой результат:

    Красный квадрат поверх жёлтого

    Лучше давать именованные цвета, а списки стилей приводить именно списком. Лучше в виде CSS-блоков.


    1. popov654 Автор
      26.11.2018 12:37

      Просто тут блоки кода нельзя выстраивать в колонки, думал, что будет не так удобно читать, и ещё менее удобно копировать.

      На самом деле, уже разобрался — это был баг инспектора. Там очень дикие вещи происходят, если менять стили на лету — в том числе и с марджинами, особенно если float элементы присутствуют в тесте. Нужна именно свежая загрузка страницы.

      P.S. Я про отладчик Chrome.


      1. MiXei4
        27.11.2018 02:31

        Невозможно читать примеры в первую очередь из-за цветов. Я не знаю, какой из этих цветов где b3ecf9, b3ecb3, и просто неудобно эти символы читать.
        Red blue yellow green подойдут намного больше по-моему.


        1. popov654 Автор
          27.11.2018 03:28

          Они выглядят слишком вырвиглазно. Мне кажется, лучше тогда дописать классы. Завтра добавлю.


  1. Beginer_First
    26.11.2018 12:37

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

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

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


    Сколько искал не нашел первых двух, автор поправь или направь меня на эти пункты ))

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


    Думаю можно было бы вставить html разметку

    Хотелось больше узнать про отрисовку элементов с указанным opacity свойством меньше единицы.
    Спасибо за статью!


    1. popov654 Автор
      26.11.2018 12:39

      Думаю можно было бы вставить html разметку

      Там есть ссылка на пример с разметкой на codepen :)

      Хотелось больше узнать про отрисовку элементов с указанным opacity свойством меньше единицы.

      А там всё просто: это альтернативно указанию свойства position, при этом z-index по умолчанию, опять же, равен auto.

      Сколько искал не нашел первых двух, автор поправь или направь меня на эти пункты ))

      Выдержка из текста:
      3. Stacking contexts formed by positioned descendants with negative z-indices (excluding 0) in z-index order (most negative first) then tree order.
      4. For all its in-flow, non-positioned, block-level descendants in tree order: If the element is a block, list-item, or other block equivalent:
      1) background color of element.
      2) background image of element.
      3) border of element.


  1. bro-dev0
    26.11.2018 20:16

    Лучше не допускать таких ситуаций когда эти знания могут пригодится.