Руководство по CSS-переменным

CSS-переменные — это настраиваемые свойства, которые обычно каскадируются и даже наследуются. Они начинаются с зарезервированного префикса --, и относительно их значения нет никаких правил или ограничений, подойдёт любое, даже простой пробел. Обработка ошибок в их значениях не происходит до той поры, пока они не будут использованы. На их значения следует ссылаться через функцию var(--name), которую можно использовать в любом CSS-свойстве. На случай, когда переменная не установлена, функция var() поддерживает второй аргумент в качестве резерва.

:root {
  --accent-color: blue;
}
body {
  background: var(--accent-color, orange);
}

Поддержка CSS-переменных браузерами

CSS variables
CSS Variables (Custom Properties). https://caniuse.com/

CSS-переменные поддерживаются в браузерах более 95% пользователей. В браузере, который не поддерживает CSS-переменные, не будет работать и функция var(). Случай уже редкий, но как и прежде, для каждой новой CSS-функции, можно использовать каскадность CSS. Вот пример:

background: red;
  background: var(--accent-color, orange);

В зависимости от реализации CSS-переменных в браузере и значения --accent-color может быть четыре варианта развития событий:

  1. Если браузер не поддерживает CSS-переменные, он будет игнорировать для background вторую строку и применит красный цвет для фона.
  2. Если браузер поддерживает CSS-переменные, значение в --accent-color соответствует цвету, это значение будет применено для фона.
  3. Если браузер поддерживает CSS-переменные, но --accent-color не установлен, будет использоваться оранжевый — это запасной вариант из второго аргумента var().
  4. Если браузер поддерживает CSS-переменные, а --accent-color будет содержать бессмысленное значение для свойства (например, 42deg вместо ожидаемого цвета), фон окажется прозрачным.

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

background: red;
background: 42deg;

В таком случае должен примениться красный цвет, т.к. браузер проигнорирует вторую строку вместе со всем, что ему будет не понятно. В примере кода, где 42deg указано в значении свойства без использования переменных, браузер избавится от неправильного свойства во время синтаксического анализа.
В случае с переменной, браузер узнает, действительно ли ее объявление, гораздо позже. К этому времени он уже отбросит остальные каскадные значения (т.к. он должен содержать только одно вычисленное значение) и, не сумев разобрать пару свойство-значение, вернётся к исходному значению — в данном случае прозрачному.

Когда резервные значения не помогают

Резервные значения для старых браузеров подходят в простых случаях, однако запросы CSS-функций — правило @supports — могут им предоставлять совершенно другой CSS. Для примера, можно установить красный фон в браузерах, которые не поддерживают CSS-переменные и зеленый фон тем, которые их поддерживают:

html { background: red; }

@supports (--css: variables) {
    html { background: green; }
}

Различия CSS-переменных и переменных препроцессора

CSS-препроцессоры — это программы, которые выполняются один раз, для генерации статического CSS-кода. В них переменные ведут себя, как переменные императивного языка программирования, с лексической областью видимости и множеством значений в процессе выполнения. Их можно использовать в любом месте таблицы стилей: селекторах, условных выражениях, свойствах, значениях и т.д., даже для генерации только фрагмента значения или селектора.

CSS-переменные можно использовать только в значениях и только для целых токенов. Они реактивны и остаются активными на протяжении всего времени существования страницы. Они используют динамическую область видимости для каждого элемента и не могут быть частью императивных вычислений, поскольку имеют только одно значение для каждого состояния. Когда CSS-переменные устанавливаются извне (например, через HTML или JavaScript), следует использовать их для чистых данных, а не для CSS-значений, таких как длина или проценты.

Динамическая вместо лексической области видимости

Область видимости переменных в препроцессорах сводится к вложенным блокам фигурных скобок. Однако, поскольку CSS-переменные являются свойствами, их область видимости (в отличие от переменных препроцессора) основана на модели DOM. Это означает, что CSS-переменные разрешаются для каждого элемента, а не для каждой области, и они наследуются как обычные свойства. Рассмотрим следующий пример CSS-переменных:

body {
  --shadow-color: gray;
}

button {
  box-shadow: .1em .1em .1em var(--shadow-color);
}

button:hover {
  --shadow-color: skyblue;
}

При наведении курсора серая тень кнопки становится небесно-голубой. Попробуем преобразовать это в язык препроцессора Sass:

body {
  $shadow-color: gray;
}

button {
  box-shadow: .1em .1em .1em $shadow-color;
}

button:hover {
  $shadow-color: skyblue;
}

Результатом будет синтаксическая ошибка: «undefined variable on line 6». Sass понятия не имеет, что <button> находится внутри <body> (потому что он выполняется не в контексте HTML, с которым имеет дело CSS в браузере), или что button: hover тоже является кнопкой.

CSS-переменные реактивны

Наиболее важным отличием от переменных препроцессора является то, что CSS-переменные являются реактивными. Они остаются активными на протяжении всего времени жизни страницы, и их обновление обновляет все отношения, которые ссылаются на них. В этом смысле они больше похожи на свойства в реактивных фреймворках, таких как Angular, Mavo и Vue, чем на переменные в традиционных языках программирования или препроцессорных языках. Поскольку они являются свойствами, их можно обновлять с помощью любого механизма, который обновляет свойства CSS: таблицы стилей, встроенные стили и даже JavaScript.

Циклы делают переменные CSS недействительными во время вычисления значения

Рассмотрим следующий фрагмент Sass:

.foo {
  $i: 1;
  z-index: $i;
  $i: $i + 1;
  z-index: $i;
}

Результат будет примерно таким:

.foo {
    z-index: 1;
    z-index: 2;
}

CSS-переменные — это совсем другая история, естественное следствие их реактивности. Посмотрим на тот же фрагмент, но с CSS-переменными:

.foo {
  --i: 1;
  z-index: var(--i);
  --i: calc(var(--i) + 1);
  z-index: var(--i);
}

Переменная, зависящая от самой себя, создала бы бесконечный цикл, которого не должно быть в декларативном языке, таком как CSS. Чтобы избежать этого, циклы делают все задействованные переменные недействительными во время вычисления значения. Сюда входят циклы длины 1 (в примере выше) и более длинные циклические цепочки, где переменная A зависит от B, которая зависит от C, и так далее до Z, которая зависит от A. Все 26 (A..Z) были бы недействительными во время вычисления значения, что сделало бы их значение равным начальному, и это по существу дало бы такой же результат, как если бы они никогда не были установлены.

CSS-переменные облегчают разделение поведения и стиля

Реактивность переменных CSS — это делает их мощными. При правильном использовании стили могут оставаться в CSS, а вычисления — в JavaScript, где им и положено находиться. Предположим, нужен радиальный градиентный фон, на котором центральная точка градиента следует за курсором мыши. Раньше приходилось генерировать весь градиент в JavaScript и устанавливать его во встроенном (inline) стиле HTML-элемента при каждом движении мыши. С CSS-переменными в JavaScript необходимо установить только две переменные: --mouse-x и --mouse-y. В обычном JavaScript это может выглядеть так:

var root = document.documentElement;

document.addEventListener("mousemove", evt => {
  let x = evt.clientX / innerWidth;
  let y = evt.clientY / innerHeight;

  root.style.setProperty("--mouse-x", x);
  root.style.setProperty("--mouse-y", y);
});

Затем надо в CSS настроить эффект, например, так:

html {
  min-height: 100vh;
  background: radial-gradient(
    at calc(100% * var(--mouse-x, .5)) calc(100% * var(--mouse-y, .5)),
    transparent, black 80%) gray;
}

See this code CSS gradient that follows the mouse with CSS variables on x.xhtml.ru.

Совершенно другой эффект будет со слоистыми градиентами, в которых используется одна и та же центральная точка:

html {
  min-height: 100vh;
  --center: calc(100% * var(--mouse-x, .5)) calc(100% * var(--mouse-y, .5));
  background: radial-gradient(circle at var(--center), yellowgreen, transparent 3%),
    conic-gradient(at var(--center), yellowgreen, green, yellowgreen);
}

See this code CSS gradients that follow the mouse with CSS variables on x.xhtml.ru.

Предположим, надо сделать, чтобы оттенки и угол в коническом градиенте менялись в зависимости от времени суток. Тот же самый JavaScript будет работать и для мыши, поэтому следует добавить немного JavaScript, который устанавливает CSS-переменную с текущим часом (0–23):

var updateHour = () => {
  var hour = new Date().getHours();
  root.style.setProperty("--hour", hour);
};
setInterval(updateHour, 60000);
updateHour();

Затем включить эту переменную в CSS и использовать её в вычислениях:

html {
  min-height: 100vh;
  --center: calc(100% * var(--mouse-x, .5)) calc(100% * var(--mouse-y, .5));
  --hue: calc(var(--hour)  * 15);
  --darkColor: hsl(var(--hue), 70%, 30%);
  --lightColor: hsl(var(--hue), 70%, 50%);
  background: conic-gradient(at var(--center),
    var(--darkColor) calc(var(--hour) / 24 * 1turn),
    var(--lightColor) 0);
}

See this code CSS gradient «clock» that follows the mouse with CSS on x.xhtml.ru.

Поскольку все три переменные, установленные с помощью JavaScript, являются чистыми данными, а не значениями из CSS, их можно использовать в нескольких несвязанных CSS-правилах. (Они не относятся непосредственно к эффектам только для фона.)

CSS-переменные упрощают инкапсуляцию стилей

CSS-переменные позволяют повторно использовать и настраивать CSS-код, поскольку они делают возможной инкапсуляцию. Предположим, есть стиль плоской кнопки, примененный к классу .flat. Его упрощенный CSS-код выглядит так:

button.flat {
  border: .1em solid black;
  background: transparent;
  color: black;
}

button.flat:hover {
  background: black;
  color: white;
}

Допустим, ещё нужны кнопки разного цвета для разных действий, например красная кнопка для опасного действия. Можно добавить поддержку класса-модификатора .danger и переопределять соответствующие объявления:

button.flat.danger {
  border-color: red;
  color: red;
}

button.flat.danger:hover {
  background: red;
  color: white;
}

Заменим цвет переменной, чтобы избежать дублирования:

button {
  --color-initial: black;
  border: .1em solid var(--color, var(--color-initial));
  background: transparent;
  color: var(--color, var(--color-initial));
}

button:hover {
  background: var(--color, var(--color-initial));
  color: white;
}

Теперь всё упростилось до переопределения одного свойства: --color:

button.flat.danger {
  --color: red;
}

Можно даже создавать темы «на лету», переопределяя свойство --color во встроенном (inline) стиле кнопки.

Это триумф краткости, но гораздо больший триумф инкапсуляции — роскошь, которую CSS не всегда позволял своим разработчикам. Фактически, натягивание тем на сторонний CSS-код раньше означало близкое знакомство с его внутренней структурой, а изменение стороннего кода требовало аналогичных обширных изменений и в коде тем. Теперь настраиваемые CSS-свойства могут служить своего рода API: кардинально меняя базовый стиль только с помощью настраиваемых свойств.

Возвращаясь к примеру с кнопкой, предположим, что надо добавить переход для более плавного эффекта наведения. Ещё надо, чтобы новый цвет фона увеличивался внутрь от границы вместо затухания по умолчанию, которое получается при переходе между двумя цветами фона. Для этого нужно имитировать фон с помощью box-shadow:

button.flat {
  --color-initial: black;
  border: .1em solid var(--color, var(--color-initial));
  background: transparent;
  color: var(--color, var(--color-initial));
  transition: 1s;
}

button.flat:hover {
  box-shadow: 0 0 0 1em var(--color, var(--color-initial)) inset;
  color: white;
}

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

Эта возможность — не экономия нескольких символов или времени на обслуживание, она является истинной ценностью настраиваемых свойств. Они позволяют делиться кодом, который может быть повторно использован и настроен другими разработчиками, при этом допускает полностью изменять его внутреннюю структуру. Пользовательские свойства также являются единственным способом создания компонентов shadow DOM, потому что, в отличие от любых других CSS-свойств, они выходят за пределы shadow DOM.

Создание цветовых палитр

Как правило, руководства по стилю начинаются с одного или нескольких основных акцентных цветов, из которых создается остальная палитра. Использование переменных только для основных цветов будет более эффективно, чем необходимость вручную настраивать каждый вариант цвета. К сожалению, в CSS пока нет функций изменения цвета. Между тем, вместо того, чтобы определить базовый цвет как одну переменную, нужно использовать отдельные переменные для компонентов цвета.

Из цветовых синтаксисов, доступных для CSS в настоящее время, hsl() имеет тенденцию работать лучше для создания цветовых вариаций (до тех пор, пока не получим lch(), который будет намного круче из-за своего более широкого диапазона и восприятия). Если понадобятся только варианты светлее/темнее и прозрачнее, можно использовать переменную как для оттенка, так и для насыщенности:

:root {
  --base-color-hs: 335, 100%;
  --base-color: hsl(var(--base-color-hs), 50%);
  --base-color-light: hsl(var(--base-color-hs), 85%);
  --base-color-dark: hsl(var(--base-color-hs), 20%);
  --base-color-translucent: hsla(var(--base-color-hs), 50%, .5);
}

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

Предотвращение наследования

Когда переменные CSS используются для хранения данных, их поведение по умолчанию — наследование — желательно. Определяем переменные в корневом элементе и можем переопределять их в поддереве. Но наследование часто мешает. Рассмотрим следующий пример, в котором используется переменная для применения тонкого свечения с предопределенным цветом, смещением и радиусом размытия, но с переменным разбросом:

* {
  box-shadow: 0 0 .3em var(--subtle-glow) gold;
}

p {
  font: 200%/1 sans-serif;
  --subtle-glow: .05em;
}

See this code Cancelling inheritance on x.xhtml.ru.

Это приводит к тому, что тонкое свечение применяется не только к элементу <p>, но и к любому из его дочерних элементов, включая ссылки <a>, выделение <em> и т.д.

Чтобы это исправить, нужно отключить наследование, добавив --subtle-glow: initial к первому правилу. Поскольку прямое приложение всегда имеет приоритет над унаследованными правилами, оно переопределит унаследованные значения, но из-за низкой специфичности * оно уступит место всему, что указано в элементе.