Как правильно задавать z-index в CSS

TL;DR

  • z-индексы могут быстро запутаться;
  • часто их задают наугад;
  • есть несколько способов поправить ситуацию, но они работают не всегда;
  • мы можем автоматизировать генерацию z-индексов и решить большинство этих проблем.

Проблема отслеживания z-индексов

z-index — относительное CSS-свойство.

У него нет единиц измерения. Результат зависит лишь от величины z-индекса относительно других таких индексов. Обычно эти значения разбросаны по всему проекту, что зачастую приводит к интересным последствиям.

Как можно запутаться в z-индексах?

Скажем, всплывающему окну было задано значение 999999.

Нужно добавить календарь, который должен располагаться позади окошка, но поверх всего остального, поэтому вы пишете 99999, оставляя немного места про запас.

А через год ваш коллега решает добавить уведомление об ошибке. Оно должно покрывать всё, в том числе и ваше окно. Он устанавливает z-index равным 9999999999. Но он не знает (или забыл) о старом компоненте, которому третий коллега (давно ушедший из проекта) выставил z-index в 999999999999.

И теперь на одной из многих страниц вашего проекта реклама будет всплывать поверх сообщения об ошибке.

Подобный баг может помешать пользователю выбрать дату, а в ином случае – и вовсе спрячет кнопку «купить»!

Использование степеней 10

Один из распространенных способов хоть как-то навести порядок – работать со степенями 10. Вы добавляете больше нулей к тому, что должно быть наверху, и меньше нулей ко всему, что ниже:

/* Это будет выше всего! */
.modal {
  z-index: 10000000;
}

/* Так стоп*/
.error {
  z-index: 10000000000;
}

Ещё одна распространённая практика: писать числа из девяток:

.modal {
  z-index: 99999999;
}

.error {
  z-index: 9999999999;
}

Последнее очень популярно.

По мере разрастания вашего проекта и привлечения большего числа разработчиков этот процесс превратится в угадайку. Очередной несчастный будет просто писать очень большое число, надеясь, что это сработает.

.old-thing {
/*Хочу, чтобы мой элемент был всегда впереди остальных*/
...
/*Этого должно быть достаточно, чтобы он вышел вперёд.*/
z-index: 1000000;
}
.new-thing {
/**
Он будет прятать всё, даже старые элементы и
то, что будет добавлено после.
**/

...
/*Просто напишу огромное число.*/
z-index: 10000000000;
}
/**
Ух ты, сколько нулей я добавил!
Правда, теперь я не могу поменять другие z-index,
Потому что не знаю, что после этого сломается,
но мне очень нужно, чтобы моя штука всегда была сверху.
**/
.newer-thing {
  z-index: 1000000000;
}

«Ой-ёй, элемент «new-thing» всё ещё спереди. Сейчас исправим…»

.newer-thing {
  z-index: 1000000000000000;
}

В результате мы получим различные числа, выбранные наугад и не имеющие смысла.

К тому же, будет труднее понять реальный порядок элементов, потому что ваши числа будут всё больше и больше. Пытаясь сравнить 1000000000000000 с 10000000000000000, вы будете вынуждены считать нули, чтобы понять, что за чем прячется.

Использование чисел, кратных 10, 100 или 1000

Тоже распространённый подход. Выглядит как-то так:

.menu {
  z-index: 100;
}
.sales-notice {
  z-index: 200;
}
.error {
  z-index: 300;
}

Это улучшенная версия предыдущего метода: уже проще сказать, какой компонент за каким находится, ведь теперь у нас есть единый порядок величин и равномерная разница между разными z-индексами.

Однако, данный метод перенял некоторые проблемы предыдущего. Если понадобится разместить новый слой между двумя уже существующими, вам придётся взять среднее значение между числами:

/**
Этот слой закрывает меню и остаётся за уведомлением о распродаже
**/

.menu {
  z-index: 100;
}
.tooltip {
  z-index: 150; /* Так-так */
}
.sales-notice {
  z-index: 200;
}
.error {
  z-index: 300;
}

Вот так мы и потеряем равномерность расстояний. А если кто-то впервые увидит код (или его новый фрагмент), он не сможет сразу разобраться, имеет ли значение меньшая разница между положением меню и всплывающей подсказки.

А ещё с ростом проекта разработчики, опять же, начнут играть в угадайку, из-за чего разбираться в значении каждой цифры станет всё труднее.

Использование объекта (или Map, или списка) для управления индексами

Здесь все z-индексы заданы в одном участке кода:

const zIndexes = {
  menu: 100,
  error: 200,
}

//Для этого используйте CSS-переменные. О них скоро поговорим.
injectZIndexes(app); 
.menu {
  z-index: var(--z-index-menu);
}
.error {
  z-index: var(--z-index-error);
}

Это значительное улучшение по сравнению с предыдущими методами. Поскольку вы управляете всеми значениями z-index в одном месте, легко увидеть их порядок. Что не менее важно, теперь, когда у них есть имена, вы можете понять их назначение.

Однако мы по-прежнему выбираем средние числа при добавлении новых слоев. Поэтому вы не сможете легко определить смысл различий в числах по мере роста вашего проекта:

const zIndexes = {
  menu: 100,
  tooltip: 125,
  modal: 150,
  error: 200,
  loadingScreen: 300
}

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

Итог

Все эти подходы сводятся к тому, что вы вынуждены выбирать случайные числа, постоянно надеясь на удачу. И во всех случаях при росте кодовой базы начинается хаос.


Лучший способ

При оптимальном варианте у нас должен быть объект с постоянной разницей между всеми слоями.

({
  'z-index-menu': 100,
  'z-index-modal': 200,
  'z-index-error': 300,
})

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

Поскольку на самом деле нам важны не точные числа, а только их относительные значения, мы можем добиться большего. Мы позволим небольшому куску кода решать всё за нас:

const makeZIndexes = (layers) =>
  layers.reduce((agg, layerName, index) => {
    const valueName = `z-index-${layerName}`;
    agg[valueName] = index * 100;

    return agg;
  }, {});
);

Используя его, мы получаем объект со всеми переменными для z-index, с удобными названиями и автоматической нумерацией:

const zIndexes = makeZIndexes(
  ['menu', 'modal', 'error']
);

// Таким образом получаем:
({
  'z-index-menu': 100,
  'z-index-modal': 200,
  'z-index-error': 300,
})

Для добавления нового слоя просто добавьте его название в массив:

const zIndexes = makeZIndexes(
  ['menu', 'clippy', 'modal', 'error']
);

// Результат:
({
  'z-index-menu': 100,
  'z-index-clippy': 200,
  'z-index-modal': 300,
  'z-index-error': 400,
})

Что мы сделали?

  • Создали маленький массив имен. Имея переменные с названиями, мы теперь легко сможем узнать, где какая используется.
  • Числа генерируются автоматически! Больше не придётся угадывать и надеяться. Мы предоставили это дело нескольким строкам кода.
  • Массив имён – единственный участок кода, через который мы общаемся с нашими z-индексами. Это очень простой интерфейс, который контролируется лишь в одном месте.
  • Значения верхних слоев изменились? Поскольку нам важна только величина z-индекса по отношению ко всем остальным, и поскольку все эти индексы автоматически упорядочены, нас это не волнует!
  • Одинаковая разница между всеми соседними слоями. Это проще читать, ведь вам не придётся вникать в смысл случайно подобранных чисел.

Как использовать эти z-индексы?

Это зависит от вашего фреймворка. Данный подход легко реализовать в любом методе, CSS-препроцессоре или фреймворке.

Например, в Vanilla (пример):

const Z_INDEX_LAYERS = ['menu', 'clippy', 'modal', 'error'];

const zIndexes = makeZIndexes(Z_INDEX_LAYERS);

// Форматируем как переменные CSS и вставляем в верхний 
// элемент HTML
const styleString = Object.entries(zIndexes)
  .map(([name, value]) => `--${name}: ${value}; `)
  .join('')

document.querySelector('.app')
  .setAttribute("style", styleString);
.menu {
  ...
  z-index: var(--z-index-menu);
}

SASS (пример):

$z-layers-names: ("menu", "sales-notice", "error");
$z-layers: ();

$i: 0;
@each $layer-name in $z-layers-names {
  $i: $i + 1;
  $css-var-name: "z-index-" + $layer-name;

  $z-layers: map-merge(
    $z-layers,
    ($css-var-name: $i),
  );
}

.menu {
  ...
  z-index: map-get($z-layers, "menu");
}

Кстати, решение для Vanilla универсально. Ваш CSS-препроцессор или «CSS-in-JS» фреймворк должны предоставлять эту возможность. Просто запустите часть с JS, внедрите в DOM и используйте переменные CSS где угодно.

Группирование связанных Z-индексов (и почему до сих пор используют кратные 100)

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

Например, кнопка закрытия связана со всплывающим окном и всегда будет идти с ним в комплекте – даже при изменении порядка слоев. Выразить это в CSS легко:

.modal-close-button {
  ...
  z-index: calc(var(--z-index-error) + 1);
}

Я реализовал это в виде универсальной npm-библиотеки Inventar plugin. Это легкий, мощный и фреймворк-независимый инструмент для управления темами и стилями в вашем проекте.

Выглядит это следующим образом:

import makeInventar from 'inventar';
import zIndex from 'inventar-z-index';

const Z_INDEX_LAYERS = ['menu', 'clippy', 'modal', 'error'];

const { inject } = makeInventar({
  ...
  zIndex: {
    value: 100,
    transformers: [zIndex(Z_INDEX_LAYERS)],
  },
});

Перевод статьи A Better Way to Manage Z-Indexes.

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

Оставьте комментарий

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

Прокрутить вверх