CSS-переменные и дизайн-токены: чек-лист перед продакшеном
Перед выкатом темы на кастомных свойствах стоит проверить каскад, фолбэки, тему по prefers-color-scheme и совместимость с SSR. Разбираем, что ломается чаще всего и как это поймать до пользователя.

Кастомные свойства CSS (те самые --color-primary) выглядят просто, пока проект маленький. На реальном продакшене они дают три класса проблем: мигание темы при загрузке, разъезжающийся каскад из-за неверной точки объявления и молчаливый провал там, где значение оказалось невалидным. Этот чек-лист — про то, что проверить перед выкатом, если вы держите дизайн-токены в CSS-переменных. Речь про нативные custom properties (спецификация CSS Custom Properties for Cascading Variables, Level 1), поддержка которых в актуальных браузерах давно полная.
Термины, чтобы не путаться
Дальше два понятия используются рядом, и их часто смешивают.
- CSS-переменная (custom property) — механизм браузера: свойство вида
--name, которое участвует в каскаде и наследуется. - Дизайн-токен1 — единица дизайн-системы:
color.brand.primaryсо значением. Токен абстрактнее и живёт в дизайне; в вебе он реализуется через CSS-переменную, но это не одно и то же.
Практический вывод: имена переменных лучше выводить из токенов машинно (Style Dictionary, собственный скрипт), а не набивать руками. Ручной ввод — источник расхождений между Figma и кодом.
Переменная, которую вы объявили, но нигде не проверили на фолбэк, — это не гибкость, а отложенный баг. Он проявится ровно в тот момент, когда значение придёт пустым.
Где объявлять корневые токены
Базовые токены (палитра, отступы, типографика) объявляют на :root. Это работает, но есть нюанс: :root — не магия, это просто селектор с невысокой специфичностью. Любое объявление той же переменной на элементе ниже по дереву перекроет корневое.
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--space-md: 16px;
}
/* тёмная тема — переопределение тех же имён */
[data-theme="dark"] {
--color-bg: #121212;
--color-text: #eaeaea;
}
Схема [data-theme] на корневом элементе предсказуема: переменная объявляется один раз, тема меняет значение. Проверьте, что переключение темы меняет атрибут именно на том элементе, от которого наследуются все компоненты (обычно <html>), а не на <body> — иначе часть портальных элементов (модалки, тултипы, отрендеренные в конец <body>) может унаследовать не то.
Фолбэки: второй аргумент var() обязателен не всегда, но полезен
Функция var() принимает второй аргумент — значение по умолчанию, если переменная не определена или содержит недопустимое значение:
.card {
/* если --card-radius не задан, применится 8px */
border-radius: var(--card-radius, 8px);
}
Тонкость, которую легко пропустить: если переменная определена, но её значение недопустимо для конкретного свойства, браузер применяет не фолбэк из var(), а initial-значение свойства (поведение guaranteed-invalid value по спецификации). То есть var(--x, 8px) спасёт только от отсутствия --x, но не от того, что кто-то присвоил --x: red и использует его в border-radius.
Перед продакшеном пройдитесь по переменным, которые приходят из JS (например, тема из localStorage или значения от пользователя): именно там значение может оказаться пустой строкой или мусором.
Мигание темы при загрузке (FOUC/FART)
Самая заметная для пользователя проблема. Тема хранится в localStorage, JS применяет её после гидрации — и на долю секунды виден светлый фон вместо тёмного. На медленном соединении это полноценная вспышка.
Лечится синхронным скриптом в <head> до отрисовки, который ставит атрибут ещё до применения стилей:
<!-- в <head>, до подключения CSS и до тела страницы -->
<script>
// читаем сохранённую тему синхронно, пока страница не отрисована
var saved = localStorage.getItem('theme');
var theme = saved
|| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
</script>
Проверка перед выкатом: откройте страницу с throttling сети (в DevTools вкладка Network, режим Slow 3G) и включённой тёмной темой. Вспышки быть не должно. Если фреймворк вставляет CSS через JS-бандл (styled-components, некоторые сборки), убедитесь, что критичные переменные темы отдаются в разметке, а не появляются после загрузки бандла.
SSR и гидрация
Если у вас Next.js, Nuxt или любой SSR, сервер не знает, какая тема сохранена у пользователя — localStorage доступен только в браузере. Наивная попытка прочитать тему в компоненте на сервере даёт рассинхрон разметки и предупреждение о hydration mismatch.
Рабочая связка: сервер рендерит нейтральный или дефолтный data-theme, а синхронный скрипт из предыдущего раздела корректирует атрибут до отрисовки. React 18+ реагирует на изменение атрибута <html> вне дерева спокойно — вы меняете DOM, а не рендерите разное на сервере и клиенте. Проверьте консоль на предупреждения гидрации в проде-сборке, а не в дев-режиме: часть предупреждений в dev шумит, часть — наоборот, приглушена.
Чек-лист перед продакшеном
- Единый источник имён. Имена переменных выводятся из токенов автоматически или хотя бы сверены со списком дизайн-системы. Осиротевших переменных (объявлены, нигде не используются) нет.
- Атрибут темы на правильном элементе.
data-themeвисит на<html>, портальные элементы наследуют корректно. - Нет мигания. Проверено на медленной сети с тёмной темой и с
prefers-color-scheme. - Фолбэки там, где значение приходит извне. Все
var()с данными из JS или пользователя имеют осмысленный второй аргумент. - SSR без mismatch. Консоль прод-сборки чиста от предупреждений гидрации.
- Контраст в обеих темах. Пары фон/текст проходят порог WCAG (минимум 4.5:1 для обычного текста). Проверяется в обеих темах, а не только в светлой.
- Печать и forced-colors. В режиме высокой контрастности (Windows) кастомные цвета могут игнорироваться системой — проверьте, что интерфейс остаётся читаемым.
Если сломалось: типичные симптомы
- Свойство просто не применилось, ошибок в консоли нет. Скорее всего значение переменной недопустимо для этого свойства и сработал initial. Проверьте вычисленное значение в DevTools (панель Computed) — там видно реальное значение, а не то, что вы ожидали.
- Тема мигает только при первом заходе. Скрипт применения темы стоит после CSS или в теле бандла. Перенесите его выше, синхронным.
- Часть UI игнорирует тёмную тему. Эти элементы либо рендерятся в портал вне
<html>сdata-theme, либо переопределяют переменную у себя. Найдите переопределение через панель Styles с фильтром по имени переменной. - Значение уехало после смены компонента. Переменная объявлена на промежуточном контейнере, а не на корне, и вы вышли из зоны его влияния. Поднимите объявление выше или сделайте его явным на компоненте.
Что настроить дальше
После выката добавьте линтинг переменных: stylelint с правилами против необъявленных custom properties ловит опечатки в именах на этапе сборки. Заведите визуальные регресс-тесты обеих тем (Playwright или Storybook + скриншот-сравнение) — тогда случайное переопределение цвета не доедет до пользователя. Смежная тема — организация слоёв токенов (примитивы → семантические → компонентные) — здесь не разбиралась; она заслуживает отдельного материала, если у вас больше двух-трёх тем.
1 Дизайн-токен — именованная пара «ключ → значение», описывающая одно дизайн-решение (цвет, отступ, радиус) независимо от платформы. В вебе реализуется через CSS-переменную, в мобильной разработке — через ресурсы платформы.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: CSS Custom Properties for Cascading Variables Module Level 1 (W3C), Using CSS custom properties (variables) — MDN
Частые вопросы
А если браузер пользователя старый и не поддерживает custom properties?
Второй аргумент var() всегда спасает от пустого значения?
Тема мигает при загрузке даже со скриптом в head — что не так?
Как поймать hydration mismatch из-за темы, если в dev всё чисто?
Стоит ли хранить токены прямо в CSS или собирать из отдельного источника?
Кастомные цвета пропали в режиме высокой контрастности Windows — это баг?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.