Динамические темы на чистом CSS: связка light-dark(), contrast-color() и style queries
Долгое время создание тем оформления требовало сложной обвязки на JavaScript или дублирования медиа-запросов @media (prefers-color-scheme). Разработчикам приходилось динамически переключать классы на элементе <body>, следить за синхронизацией состояния и вручную рассчитывать контрастность текста для различных фонов, чтобы интерфейс оставался доступным.
С приходом современных стандартов CSS (интегрированных в Baseline 2026) ситуация изменилась. Браузеры получили нативную поддержку инструментов, позволяющих решать задачи макро- и микро-темизации полностью декларативно. Связка функций light-dark(), contrast-color() и контейнерных запросов по стилю (style queries) позволяет создавать адаптивные интерфейсы, которые автоматически подстраивают цвета текста, тени и свечение под цветовую схему и цвет бренда без JavaScript.
Макро- и микро-темизация: в чем разница?
Для построения надежной дизайн-системы важно разделить два понятия:
- Макро-темизация отвечает на вопрос глобального окружения: «В каком режиме отображается страница или ее раздел — в светлом или темном?». Этот уровень управляет общим фоном страницы, системными элементами (такими как полосы прокрутки) и базовыми текстовыми стилями.
- Микро-темизация работает на уровне отдельных компонентов. Она решает локальные задачи: например, «Какого цвета должен быть текст на этой карточке, если ее фон динамически меняется на цвет бренда?» или «Как должна измениться тень элемента при переходе в темный режим?».
Современный CSS позволяет разделить эти уровни, делегируя макро-настройки свойству color-scheme и функции light-dark(), а микро-настройки — функции contrast-color() и запросам @container style().
Пошаговое руководство по созданию динамической темы
Ниже приведено практическое руководство по внедрению современной системы темизации.
Предварительные требования и подготовка
- Проект допускает использование функций категории Baseline Newly Available (недавно ставших доступными).
- В команде определена базовая палитра и подготовлена стратегия прогрессивного улучшения (progressive enhancement) для старых браузеров.
- Тестирование контрастности готовых компонентов проводится визуально и программно, так как автоматические алгоритмы могут не учитывать эстетические особенности бренда.
Шаг 1. Глобальное объявление цветовой схемы страницы
Для того чтобы браузер понимал, какие цветовые схемы поддерживает приложение, добавьте свойство color-scheme на корневой элемент:
html {
color-scheme: light dark;
}
Что делает свойство: сообщает браузеру, что документ поддерживает как светлую, так и темную тему. Это переключает системные цвета форм, полос прокрутки и стандартных поверхностей, а также активирует работу функции light-dark().
Проверка: измените системную тему ОС — стандартные элементы интерфейса должны автоматически изменить свой цвет.
Шаг 2. Определение глобального фона страницы
Задайте базовые переменные для фона страницы с помощью функции light-dark():
body {
--page-bg: light-dark(#f6f8fb, #090b10);
background: var(--page-bg);
}
Что делает код: функция light-dark() принимает два аргумента. Первый используется, когда computed-значение color-scheme равно light, а второй — когда dark. Она реагирует именно на значение color-scheme родительского контейнера. Это позволяет принудительно переключать тему для отдельных блоков (например, задав .dark-section { color-scheme: dark; }).
Шаг 3. Задание фонов компонентов и автоматический расчет контраста
Каждому компоненту задается собственный фоновый цвет. Текст внутри компонента должен автоматически подстраиваться под этот фон:
.card {
--bg: var(--brand-color);
background: var(--bg);
color: contrast-color(var(--bg));
}
Что делает код: функция contrast-color(var(--bg)) анализирует цвет фона и автоматически выбирает из встроенной палитры наиболее контрастный цвет текста (черный или белый). Это исключает ситуации, когда темный текст накладывается на темный брендовый фон.
Шаг 4. Регистрация типизированного свойства для логического ветвления
Чистые черный и белый цвета текста часто выглядят слишком агрессивно на цветных подложках. Чтобы заменить их на мягкие оттенки с примесью основного тона, необходимо использовать контейнерные запросы по стилю. Сначала зарегистрируем пользовательское свойство через @property:
@property --contrast-color {
syntax: "<color>";
initial-value: white;
inherits: true;
}
.card {
--contrast-color: contrast-color(var(--bg));
color: var(--contrast-color);
}
Зачем это нужно: регистрация свойства через @property сообщает CSS-движку, что переменная --contrast-color содержит значение типа <color>. Без этого браузер будет воспринимать переменную как сырую строку и не сможет сопоставить ее в логических выражениях @container style().
Шаг 5. Настройка палитры через контейнерные запросы по стилю (Style Queries)
Напишите правила ветвления, которые заменят жесткие контрастные цвета на более гармоничные оттенки, полученные с помощью относительного синтаксиса цветов:
@container style(--contrast-color: white) {
.card-label,
.card-body {
color: oklch(from var(--bg) 0.9 0.05 h);
}
}
@container style(--contrast-color: black) {
.card-label,
.card-body {
color: oklch(from var(--bg) 0.2 0.05 h);
}
}
Что делает код: если функция contrast-color() выбрала белый цвет, срабатывает первый контейнерный запрос. Он окрашивает текст в светлый оттенок, унаследованный от цветового тона (hue) фона var(--bg) с яркостью 90% и хроматичностью 5%. Если нужен черный текст — второй запрос делает текст темным (яркость 20%).
Шаг 6. Настройка эффектов глубины (Elevation) без дублирования кода
В светлой теме карточки должны отбрасывать мягкую тень, а в темной — иметь тонкую обводку и внутреннее свечение. Используем трюк с прозрачностью слоев:
.card {
box-shadow:
0 1px 2px light-dark(rgba(0, 0, 0, 0.1), transparent),
0 4px 12px light-dark(rgba(0, 0, 0, 0.08), transparent),
inset 0 0 0 1px light-dark(transparent, oklch(from var(--bg) calc(l + 0.4) c h / 0.4)),
0 0 16px light-dark(transparent, oklch(from var(--bg) calc(l + 0.4) c h / 0.25));
}
Зачем это нужно: геометрия тени остается неизменной, но цвета слоев переключаются динамически. В светлой схеме внешние тени становятся видимыми, а внутреннее свечение — прозрачным. В темной схеме внешние тени исчезают, уступая место внутреннему бортику и внешнему неоновому ореолу, цвет которого вычисляется из фонового цвета компонента var(--bg) через oklch().
Шаг 7. Оптимизация через CSS-функции (опционально)
Если ваш стек поддерживает экспериментальные возможности спецификации, вы можете вынести описание теней в повторно используемую функцию:
@function --elevation(--bg) {
result: 0 1px 2px light-dark(rgba(0,0,0,0.1), transparent),
0 4px 12px light-dark(rgba(0,0,0,0.08), transparent),
inset 0 0 0 1px light-dark(transparent, oklch(from var(--bg) calc(l + 0.4) c h / 0.4));
}
Предостережение: на данный момент поддержка директивы @function ограничена. Не используйте ее в продакшене без этапа сборки/препроцессинга.
Риски и стратегии безопасного внедрения
- Совместимость с браузерами: функции
light-dark()иcontrast-color()получили статус Baseline Newly Available в первой половине 2026 года. Это означает, что они работают во всех современных версиях Chrome, Safari и Firefox, однако могут не поддерживаться в старых корпоративных дистрибутивах или встроенных WebView. - Стратегия фолбеков (Progressive Enhancement): всегда задавайте базовые читаемые цвета перед использованием новых функций. Браузеры, не поддерживающие
contrast-color(), проигнорируют это свойство, поэтому необходимо предоставить им надежный резервный вариант:.card { background: #0070f3; color: #ffffff; background: var(--bg); color: contrast-color(var(--bg)); } - Ручной контроль контрастности: функция
contrast-color()гарантирует математическую доступность (черный или белый текст). Однако, если вы переопределяете цвета через@container style(), добавляя художественное тонирование, обязательно проверяйте конечные компоненты на соответствие пороговым значениям контраста WCAG AA.

