CSS-переменные (custom properties)

Урок про нативные переменные CSS: как объявить, прочитать, унаследовать и менять их прямо в браузере без сборки.

CSS-переменная (custom property) — это объявление вида --имя: значение, которое живёт в дереве элементов, наследуется потомками и читается функцией var(--имя) прямо во время работы страницы.

Зачем это на практике

Раньше, чтобы поменять основной цвет темы, приходилось искать его десятки раз по всему файлу или гонять препроцессор. CSS-переменные убирают эту боль: вы объявляете значение один раз, а используете его сколько угодно. Когда дизайнер просит «сделать акцент чуть темнее», вы правите одну строку. Но главная сила в другом — в отличие от переменных Sass, нативные переменные живут в браузере и их можно читать и менять во время работы страницы: по клику, по медиа-запросу, из JavaScript. Именно на них держатся современные системы тем (светлая/тёмная) и дизайн-токены.

Объявление и чтение

Переменную объявляют как обычное свойство, но имя начинается с двух дефисов. Чаще всего её кладут в псевдокласс :root — это корень документа (элемент html), и оттуда значение видно всей странице.

:root {
  --brand: #2563eb;
  --space: 16px;
  --radius: 8px;
}

.button {
  background: var(--brand);
  padding: var(--space);
  border-radius: var(--radius);
}

Функция var(--brand) подставляет текущее значение переменной. Имена чувствительны к регистру: --brand и --Brand — разные переменные. Значением может быть что угодно: цвет, число с единицей, целый список (--shadow: 0 1px 3px rgba(0,0,0,0.2)) и даже кусок значения.

Наследование и каскад

Главное отличие от препроцессорных переменных: custom properties участвуют в наследовании. Объявленная на элементе переменная видна ему и всем его потомкам, но не родителям и не соседям. Это позволяет переопределять значение точечно — для отдельной ветки дерева.

:root { --text: #1f2937; }

/* Внутри тёмной карточки переопределяем только здесь */
.card--dark {
  --text: #f9fafb;
  background: #111827;
}

p { color: var(--text); }

Абзац внутри .card--dark получит светлый текст, а такой же абзац снаружи — тёмный. Никаких дополнительных селекторов вроде .card--dark p не понадобилось: переменная «спустилась» по дереву. Работает и обычный каскад — если две одинаково специфичные точки объявят --text, победит та, что ниже в коде.

Запасное значение (fallback)

Вторым аргументом var() можно передать запасное значение — оно используется, если переменная не определена или её значение невалидно для этого свойства.

.box {
  /* если --gap нигде не задана — возьмём 12px */
  gap: var(--gap, 12px);
  /* fallback может быть другой переменной */
  color: var(--accent, var(--brand, black));
}

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

Темизация: светлая и тёмная

Самый частый реальный сценарий. Собираем палитру в переменные, а в тёмной теме переопределяем те же имена. Компонентам всё равно — они читают var(--bg) и var(--fg).

:root {
  --bg: #ffffff;
  --fg: #111827;
  --card: #f3f4f6;
}

/* Тема по выбору пользователя */
[data-theme="dark"] {
  --bg: #0b1120;
  --fg: #e5e7eb;
  --card: #1f2937;
}

/* Либо по системной настройке */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0b1120;
    --fg: #e5e7eb;
    --card: #1f2937;
  }
}

body { background: var(--bg); color: var(--fg); }
.card { background: var(--card); }

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

Изменение из JavaScript

Поскольку переменные — часть живого CSSOM, их читают и пишут через обычный API стилей. Это мост между скриптом и оформлением без хардкода цветов в JS.

/* CSS объявляет токен */
:root { --accent: #2563eb; }
.badge { background: var(--accent); }
// Читаем текущее значение
const root = document.documentElement;
const styles = getComputedStyle(root);
console.log(styles.getPropertyValue("--accent").trim());

// Задаём новое — все .badge перекрасятся
root.style.setProperty("--accent", "#dc2626");
console.log("accent updated");

Вывод:

#2563eb
accent updated

Так делают ползунки настройки темы, выбор «акцентного цвета» в интерфейсе и анимации, где значение переменной плавно меняют через requestAnimationFrame.

Отличие от переменных препроцессора

Переменные Sass/Less ($brand, @brand) и нативные --brand легко спутать, но это принципиально разные вещи.

СвойствоSass $varCSS --var
Когда вычисляетсяпри компиляции, до отдачи браузерув браузере, во время работы
Есть в готовом CSSнет, заменяется на значениеда, остаётся в коде
Наследуется по деревунет (область видимости — текст)да (область видимости — DOM)
Меняется из JS / по теменельзяможно
Видна в DevToolsнетда

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

Как это работает под капотом

Custom property — это обычное наследуемое свойство, значение которого браузер хранит как «токены» почти без проверки. Валидность проверяется только в момент подстановки через var(): вот тогда движок берёт вычисленное значение из ближайшего предка, где оно объявлено, и пытается применить к конкретному свойству. Если результат бессмыслен (например, в color попало 16px), свойство получает значение unset и в дело идёт наследование или начальное значение — а не fallback из var() (он срабатывает лишь когда сама переменная пуста). Поэтому опечатка в значении переменной иногда «молча» ломает один цвет, а не всю страницу.

Частые ошибки

  • Забыть два дефиса. var(-brand) или var(brand) не работают — имя обязано начинаться с --.
  • Ждать каскад «вверх». Переменная, объявленная на потомке, не видна родителю. Кладите общие токены в :root.
  • Путать fallback и невалидное значение. Если переменная задана, но её значение не подходит свойству, второй аргумент var() не спасёт — свойство станет unset.
  • Считать их Sass-переменными. В @media нельзя написать @media (min-width: var(--bp)) — переменные не работают в самих медиа-запросах (только в значениях свойств).

Итоги

  • Объявление — --имя: значение, чтение — var(--имя, запасное).
  • Переменные наследуются по DOM-дереву и подчиняются каскаду — это позволяет переопределять токены точечно.
  • Темы строятся переопределением одних и тех же имён в :root и [data-theme].
  • Значения читаются и пишутся из JavaScript через getPropertyValue / setProperty.
  • В отличие от Sass-переменных, нативные живут в рантайме, видны в DevTools и меняются на лету.
Проверьте себя
1. Чем CSS-переменная (--var) принципиально отличается от переменной Sass ($var)?
AНичем, это синонимы для одного и того же механизма
BCSS-переменная вычисляется в браузере в рантайме, наследуется по DOM и может меняться из JS; Sass-переменная подставляется при компиляции
CSass-переменная наследуется по дереву, а CSS-переменная — нет
DCSS-переменную нельзя использовать в свойстве color
2. Что выведет код, если переменная --gap нигде не объявлена: gap: var(--gap, 12px);?
AСвойство будет проигнорировано
BОшибку парсинга
Cgap станет равным 12px — это запасное значение
Dgap унаследует значение от родителя
3. Как переопределить токен --text только для потомков элемента .card, не трогая остальную страницу?
AОбъявить --text заново внутри правила .card { } — он унаследуется только вниз по дереву
BИзменить --text в :root — других способов нет
CЗаписать .card --text: ... через JS
DЭто невозможно без отдельного селектора .card p