CSS Custom Properties, они же CSS-переменные, хранят значение в одном месте и
подставляют его куда угодно. На них держатся темы, наборы отступов и цветовые
палитры, которые правятся одной строкой вместо поиска по всему файлу.
Отдельно они хороши в связке с движением. Переменную можно менять из
@keyframes или по :hover, и одно значение перестроит сразу несколько
свойств. Как это устроено, разбираем в статье про
CSS-анимации.
Базовый синтаксис и использование
Объявление CSS переменных
:root {
/* Глобальные переменные */
--primary-color: #3498db;
--secondary-color: #2ecc71;
--spacing-unit: 1rem;
--border-radius: 8px;
}
.component {
/* Локальные переменные */
--component-padding: calc(var(--spacing-unit) * 2);
--component-bg: #f8f9fa;
}
Использование CSS переменных
.button {
background-color: var(--primary-color);
padding: var(--spacing-unit);
border-radius: var(--border-radius);
/* С резервным значением */
color: var(--button-text-color, white);
}
Область видимости и наследование
Глобальная и локальная область
:root {
--global-color: blue;
}
.parent {
--local-color: red;
color: var(--local-color); /* красный */
}
.child {
/* Наследует от родителя */
background: var(--local-color); /* красный */
border-color: var(--global-color); /* синий */
}
Переопределение переменных
:root {
--spacing: 1rem;
}
.compact {
--spacing: 0.5rem;
}
.spacious {
--spacing: 2rem;
}
.element {
padding: var(--spacing);
/* Отступы меняются в зависимости от родительского класса */
}
Перевести цвет между HEX, RGB и HSL. Светлота в HSL как раз и нужна для тем:
Динамическая темизация
Светлая и тёмная тема
:root {
/* Светлая тема (по умолчанию) */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #dee2e6;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Тёмные значения выносим в один набор, чтобы не дублировать их дважды */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--border-color: #404040;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
}
/* Ручной выбор пользователя перебивает системную настройку */
[data-theme='dark'] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--border-color: #404040;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
/* Компоненты автоматически адаптируются */
body {
background-color: var(--bg-primary);
color: var(--text-primary);
transition:
background-color 0.3s ease,
color 0.3s ease;
}
/* Плавное переключение темы — приятно, но не всем: анимация фона на весь экран
способна вызвать дискомфорт, поэтому уважаем системную настройку */
@media (prefers-reduced-motion: reduce) {
body {
transition: none;
}
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
box-shadow: var(--shadow);
padding: 1.5rem;
border-radius: 8px;
}
Интеграция с JavaScript
Чтение CSS переменных
// Получение вычисленных стилей
const styles = getComputedStyle(document.documentElement)
const primaryColor = styles.getPropertyValue('--primary-color')
// Получение от конкретного элемента
const element = document.querySelector('.component')
const elementStyles = getComputedStyle(element)
const spacing = elementStyles.getPropertyValue('--spacing')
Установка CSS переменных
// Установка на корне
document.documentElement.style.setProperty('--primary-color', '#e74c3c')
// Установка на конкретном элементе
element.style.setProperty('--component-height', '200px')
// Динамические обновления
const updateThemeColor = hue => {
document.documentElement.style.setProperty('--primary-h', hue)
}
Продвинутые техники
Манипуляции с цветом
Долгое время цвет разбирали на составляющие и складывали обратно через calc():
:root {
--primary-h: 210;
--primary-s: 100%;
--primary-l: 50%;
--primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
--primary-light: hsl(
var(--primary-h) var(--primary-s) calc(var(--primary-l) + 20%)
);
}
Приём рабочий, но у него две беды. Во-первых, цвет перестаёт быть цветом, его
негде посмотреть целиком, везде только обрывки. Во-вторых, арифметика молча
ломается на краях. У светлого цвета с --primary-l: 90% прибавка +20% даст
110%, и вариация окажется просто белой.
Сегодня для этого есть два честных инструмента, и оба Baseline.
Функция color-mix() подмешивает белый или чёрный:
:root {
--primary: #0d6efd;
/* Светлее: подмешиваем белый */
--primary-light: color-mix(in srgb, var(--primary), white 20%);
/* Темнее: подмешиваем чёрный */
--primary-dark: color-mix(in srgb, var(--primary), black 20%);
/* Полупрозрачный — без дублирования каналов */
--primary-alpha: color-mix(in srgb, var(--primary), transparent 80%);
}
Относительный синтаксис меняет отдельный канал, не разбирая цвет:
:root {
--primary: #0d6efd;
/* from — берём каналы исходного цвета и правим только светлоту */
--primary-light: hsl(from var(--primary) h s calc(l + 20%));
--primary-dark: hsl(from var(--primary) h s calc(l - 20%));
}
Главный выигрыш в том, что базовый цвет остаётся одним значением, которое можно
скопировать в макет и обратно. Поменяли --primary, и вся палитра пересчиталась
сама.
Адаптивная типографика
Здесь легко навредить, поэтому два правила сразу.
Базовый размер задавайте в rem, а не в px. Размер в пикселях игнорирует
настройку шрифта в браузере, и человек, который выставил себе крупный шрифт,
потому что плохо видит, не получит ничего. 1rem эту настройку уважает.
Не понижайте базовый размер на мобильных. Экран меньше, а глаза те же, и телефон
держат ближе. Уменьшать текст до 14px там, где читать и так труднее, —
распространённая, но вредная привычка.
Масштабировать стоит шкалу. На большом экране заголовки могут расходиться
сильнее, а основной текст остаётся тем же.
:root {
--font-size-base: 1rem; /* уважает настройки пользователя */
--font-size-scale: 1.2; /* компактная шкала для мобильных */
--font-size-sm: calc(var(--font-size-base) / var(--font-size-scale));
--font-size-md: var(--font-size-base);
--font-size-lg: calc(var(--font-size-base) * var(--font-size-scale));
--font-size-xl: calc(var(--font-size-lg) * var(--font-size-scale));
}
/* Растёт контраст между заголовками, базовый текст не трогаем */
@media (min-width: 768px) {
:root {
--font-size-scale: 1.333;
}
}
Плавную альтернативу даёт clamp(), с ним размер тянется вместе с экраном без
единого медиазапроса.
Что учитывать на практике
- Держите единые префиксы в именах:
--color-, --space-, --font-.
- Для стилей, без которых страница развалится, указывайте резервное значение
вторым аргументом
var().
- Комментируйте вычисления, через месяц
calc() из трёх переменных читается
плохо.
- Группируйте связанные переменные рядом, а не по алфавиту.
- Системные переменные объявляйте в
:root, локальные — там, где они нужны.
Переменные окупаются не в маленьком проекте, а в тот момент, когда дизайнер
просит поменять акцентный цвет. Правится одна строка вместо тридцати.