Динамические темы на кастомных свойствах: как сделать это красиво и не сойти с ума
Присаживайся, наливай кофе. Сегодня поговорим о вещи, которая стабильно прилетает в таски на любом живом проекте. Рано или поздно к тебе приходит тимлид или дизайнер и говорит: «Нам нужна темная тема. И еще розовая для промо-кампании. И чтобы пользователь мог сам выбирать акцентный цвет в личном кабинете. И чтоб все это переключалось без перезагрузки страницы, плавно и без костылей».
Если первая мысль, которая у тебя возникает — это создать десять разных CSS-файлов или обмазаться классами типа .dark-theme на каждом кнопке, выдохни. Мы живем в 2026 году. У нас есть кастомные свойства (они же CSS-переменные), и они решают эту задачу изящно, в несколько строк кода. Давай разберем, как сделать архитектуру тем гибкой, масштабируемой и приятной в поддержке.
Как мы страдали раньше
Вспомни (или содрогнись, если не застал), как мы выкручивались во времена расцвета SASS и LESS. Мы создавали переменные вроде $brand-color: #3b82f6;. Но препроцессоры компилируются на сервере или в процессе сборки. В браузере они превращаются в обычный статичный CSS. Динамики там ноль.
Чтобы сделать темную тему, нам приходилось писать тонны дублирующего кода:
/* Старый кошмар */
.button {
background-color: #ffffff;
color: #333333;
}
body.dark-theme .button {
background-color: #1a1a1a;
color: #ffffff;
}
Представь масштаб трагедии, когда проект разрастается до сотен компонентов. Ты получаешь бесконечные простыни переопределений, воюешь со специфичностью селекторов и постоянно забываешь покрасить какую-нибудь мелкую иконку. Кстати, если ты подзабыл базовые принципы работы с современными переменными, обязательно почитай о том, почему переменные (CSS Variables) — это основа масштабируемого дизайна.
Как делать правильно в 2026 году
Современный подход строится на разделении структуры и темы. Твои компоненты вообще не должны знать, какого они цвета. Они должны знать только одно: «я использую переменную --bg-primary для фона».
Мы объявляем базовые семантические переменные на уровне :root, а затем просто переопределяем их значения в зависимости от атрибута (например, data-theme), который вешаем на тег html или body. Это позволяет переключать темы одной строчкой на JavaScript.
Более того, мы можем использовать медиа-запросы для автоматического определения системных настроек пользователя (светлая/темная тема в ОС), а JS использовать только для ручного переопределения.
Если хочешь пойти еще дальше и делать сложные анимации переходов или строго типизировать значения, загляни в наш гайд по строгой типизации CSS-переменных с правилом @property. Ну а для классического гибкого переключения тем нам хватит стандартных кастомных свойств.
Готовый сниппет кода
Вот рабочий, чистый шаблон, который ты можешь забрать в свой проект прямо сейчас. Здесь настроена светлая тема по умолчанию, автоматическая темная тема под системные настройки и ручной переключатель на JavaScript.
<!-- HTML -->
<!DOCTYPE html>
<html lang="ru" data-theme="auto">
<head>
<meta charset="UTF-8">
<title>Dynamic Themes</title>
</head>
<body>
<div class="card">
<h1>Привет, мидл!</h1>
<p>Переключай тему и смотри, как магия CSS делает всю грязную работу.</p>
<button id="theme-toggle" class="btn">Сменить тему</button>
</div>
</body>
</html>
/* CSS */
/* 1. Семантические переменные для светлой темы */
:root {
--bg-app: #f4f4f9;
--bg-card: #ffffff;
--text-main: #1a1a1a;
--accent: #3b82f6;
--border-color: #e5e7eb;
}
/* 2. Автоматическая темная тема (через системные настройки) */
@media (prefers-color-scheme: dark) {
:root[data-theme="auto"] {
--bg-app: #0f172a;
--bg-card: #1e293b;
--text-main: #f8fafc;
--accent: #60a5fa;
--border-color: #334155;
}
}
/* 3. Принудительная темная тема при ручном переключении */
:root[data-theme="dark"] {
--bg-app: #0f172a;
--bg-card: #1e293b;
--text-main: #f8fafc;
--accent: #60a5fa;
--border-color: #334155;
}
/* 4. Стилизация компонентов (никакого хардкода цветов!) */
body {
background-color: var(--bg-app);
color: var(--text-main);
font-family: system-ui, sans-serif;
transition: background-color 0.3s ease, color 0.3s ease;
display: grid;
place-items: center;
min-height: 100vh;
margin: 0;
}
.card {
background-color: var(--bg-card);
border: 1px solid var(--border-color);
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
max-width: 400px;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.btn {
background-color: var(--accent);
color: #ffffff;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: filter 0.2s ease;
}
.btn:hover {
filter: brightness(1.1);
}
// JavaScript для переключения темы
const toggleBtn = document.getElementById('theme-toggle');
const htmlElement = document.documentElement;
toggleBtn.addEventListener('click', () => {
const currentTheme = htmlElement.getAttribute('data-theme');
let newTheme = 'light';
if (currentTheme === 'light' || currentTheme === 'auto') {
newTheme = 'dark';
}
htmlElement.setAttribute('data-theme', newTheme);
// Опционально сохраняем выбор пользователя в localStorage
localStorage.setItem('user-theme', newTheme);
});
Частая ошибка новичков
Самый частый факап, который я вижу на код-ревью у ребят, только пришедших на проект — это буквальное (литеральное) именование переменных.
Никогда. Слышишь? Никогда не пиши вот так:
/* ТАК ДЕЛАТЬ НЕ НАДО */
:root {
--white: #ffffff;
--black: #000000;
}
[data-theme="dark"] {
--white: #000000; /* Взрыв мозга гарантирован */
--black: #ffffff;
}
Когда у тебя в темной теме переменная --white начинает возвращать черный цвет, код превращается в театр абсурда. Вся команда начнет плеваться уже на второй день поддержки.
Правило простое: именуй кастомные свойства по их роли в интерфейсе (семантически), а не по конкретному значению. Вместо цвета пиши его назначение: --bg-primary, --text-muted, --border-active. Тогда при смене темы тебе нужно будет поменять только значения, а логика останется кристально чистой.
На этом всё. Внедряй кастомные свойства, береги нервы коллег и пиши красивый CSS!
🔥 Больше фишек, готовых сниппетов и передовых подходов к CSS мы публикуем в нашем Telegram-канале. Подписывайтесь, чтобы не пропустить!