
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]


