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]