Создание переключателя тем с помощью CSS-переменных
Жёстко прописанные значения цветов, разбросанные по таблице стилей, превращают смену темы в мучение. Меняя один цвет, приходится перерывать десятки объявлений. CSS-переменные решают эту проблему элегантно — определите свои дизайн-токены один раз, а затем переключайте темы простым изменением одного атрибута на корневом элементе.
В этой статье мы пошагово создадим поддерживаемый переключатель тем: тот, что автоматически учитывает системные настройки, реагирует на ручной выбор пользователя и сохраняет этот выбор между сессиями.
Ключевые выводы
- CSS custom properties динамичны и обновляются во время выполнения, что делает их идеальными дизайн-токенами для тематизации.
- Атрибут
data-themeна корневом элементе обеспечивает чистый, масштабируемый способ переключения между любым количеством тем. - Медиа-запрос
prefers-color-schemeобрабатывает системные значения по умолчанию, а JavaScript иlocalStorageсохраняют ручной выбор пользователя. - Небольшой встроенный скрипт в
<head>предотвращает мигание неправильной темы при загрузке страницы. - Свойство
color-schemeгарантирует, что нативный интерфейс браузера адаптируется под активную тему.
Почему CSS-переменные — правильная основа для тематизации
CSS custom properties динамичны. В отличие от переменных Sass, которые компилируются в статические значения, CSS-переменные живут в браузере и могут обновляться во время выполнения — медиа-запросами, JavaScript или родительским элементом, меняющим состояние.
Это делает их идеальными дизайн-токенами. Определите цвета поверхностей, текста и акцентов как переменные в :root, а затем ссылайтесь на них по всей таблице стилей. Переключение тем превращается в одно изменение DOM, а не в замену таблицы стилей.
Настройка токенов темы
Начните с определения светлой темы как темы по умолчанию, а затем объявите тёмный вариант с помощью селектора атрибута data-theme:
:root {
color-scheme: light;
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-primary: #302ae6;
--color-surface: #f4f4f4;
}
[data-theme="dark"] {
color-scheme: dark;
--color-bg: #161625;
--color-text: #e1e1ff;
--color-primary: #9a97f3;
--color-surface: #1e1e30;
}
Свойство color-scheme стоит включить. Оно сообщает браузеру, какой режим активен, чтобы нативные элементы интерфейса — полосы прокрутки, поля форм, кольца фокуса — автоматически адаптировались под вашу тему.
Теперь используйте эти токены по всей таблице стилей:
body {
background-color: var(--color-bg);
color: var(--color-text);
}
Учёт системных предпочтений с помощью prefers-color-scheme
Системные предпочтения и ручное переключение тем решают разные задачи. prefers-color-scheme обрабатывает автоматический случай — пользователей, которые настроили ОС на тёмный режим и ожидают, что сайты будут этому соответствовать:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--color-bg: #161625;
--color-text: #e1e1ff;
--color-primary: #9a97f3;
--color-surface: #1e1e30;
}
}
Защита :not([data-theme="light"]) гарантирует, что явный выбор пользователя перекрывает системное значение по умолчанию. Без неё медиа-запрос переопределил бы ручной выбор.
Несколько слов о light-dark()
CSS-функция light-dark() позволяет объявить оба значения темы прямо в строке:
:root {
color-scheme: light dark;
--color-bg: light-dark(#ffffff, #161625);
}
Это элегантное решение для более простых наборов токенов и хорошо работает, когда вам нужны только две темы. Для систем с более чем двумя темами или с токенами, варьирующимися не только по цвету, подход с селектором атрибута даёт больше контроля.
Обратите внимание, что для корректной работы light-dark() свойство color-scheme должно быть задано с обоими значениями. Поддержка в современных браузерах сильная, но всё же относительно недавняя (Chrome 123+, Safari 17.5+, Firefox 120+), поэтому, прежде чем полагаться на неё, проверьте совместимость с вашей аудиторией.
Discover how at OpenReplay.com.
Добавление JavaScript для постоянного ручного управления
Определение системных предпочтений возможно только на CSS, но запоминание ручного выбора пользователя требует JavaScript. Вот минимальная, готовая к продакшену реализация:
const STORAGE_KEY = 'theme-preference';
const root = document.documentElement;
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function getSavedTheme() {
try {
return localStorage.getItem(STORAGE_KEY);
} catch {
return null;
}
}
function applyTheme(theme) {
root.setAttribute('data-theme', theme);
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch {
// Storage unavailable — theme still applies for this session
}
}
// On load: respect saved preference, fall back to system
applyTheme(getSavedTheme() || getSystemTheme());
// Wire up your toggle button
document.getElementById('theme-toggle').addEventListener('click', () => {
const current = root.getAttribute('data-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
});
// Sync with OS changes when no manual preference is set
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!getSavedTheme()) {
root.setAttribute('data-theme', e.matches ? 'dark' : 'light');
}
});
Блоки try/catch вокруг localStorage корректно обрабатывают режимы приватного просмотра и ошибки квоты хранилища — тема всё равно применится для текущей сессии, даже если её нельзя сохранить.
Предотвращение мигания неправильной темы
Если ваш JavaScript загружается после рендеринга страницы, пользователи на короткое время видят тему по умолчанию, прежде чем применится правильная. Это решается с помощью небольшого блокирующего встроенного скрипта в <head>:
<script>
try {
const saved = localStorage.getItem('theme-preference');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
} catch {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
}
</script>
Этот код выполняется синхронно до рендеринга какого-либо контента, устраняя мигание. Разместите его как можно раньше в <head>, перед любыми таблицами стилей, чтобы атрибут существовал к моменту первого применения CSS.
Шаблон, который масштабируется
CSS-переменные обеспечивают чёткое разделение между дизайн-токенами и стилями компонентов. Компоненты ссылаются на переменные; темы определяют переменные. Добавление третьей темы — высококонтрастной, брендового варианта, сезонной палитры — означает добавление одного нового блока селектора атрибута в CSS и одной новой опции в логику переключателя. Больше ничего менять не нужно.
Заключение
Реальная ценность здесь не просто в тёмном режиме — это архитектура тематизации, которая остаётся поддерживаемой по мере роста вашей дизайн-системы. Рассматривая CSS-переменные как дизайн-токены, учитывая как системные, так и пользовательские предпочтения и обрабатывая граничные случаи (сбои хранилища, мигания при рендеринге, изменения на уровне ОС), вы создаёте переключатель, который ощущается нативным для платформы и тривиально расширяемым. Шаблон невелик, а выгода накапливается с каждой новой темой или компонентом.
Часто задаваемые вопросы
Оба варианта работают, но атрибут data-theme обычно чище, поскольку выражает одно значение, а не список модификаторов. Классы лучше подходят для композиционного состояния, где сосуществуют несколько флагов. С темами у вас обычно одно активное значение за раз, что естественнее соответствует семантике селектора атрибута.
Это мигание происходит, когда ваш JavaScript выполняется уже после того, как браузер отрисовал страницу со стилями по умолчанию. Решение — небольшой синхронный встроенный скрипт в head, который читает сохранённое значение из localStorage и устанавливает атрибут data-theme до начала рендеринга. Для первых посетителей можно также проверить prefers-color-scheme, чтобы первичный рендер сразу соответствовал системной теме.
Да. CSS-переменные могут содержать любое допустимое CSS-значение, включая шкалы отступов, радиусы скруглений, тени, наборы шрифтов и продолжительности анимаций. Это делает их полезными для тематизации плотности (компактные и комфортные раскладки), предпочтений движения или даже типографических вариантов. Рассматривайте переменные как дизайн-токены в широком смысле, а не только как ссылки на цвета.
Она хорошо работает в современных браузерах (Chrome 123+, Safari 17.5+, Firefox 120+), но не поддерживается в более старых версиях. Для проектов с современной аудиторией это чистый выбор. Для более широкой совместимости подход с атрибутом data-theme остаётся безопаснее и также масштабируется на более чем две темы, чего light-dark() сама по себе сделать не может.
Truly understand users experience
See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..