Как правильно сделать тёмную тему на сайте
Практика3 минРедакция Innovialabs
Коротко
Опишите цвета через CSS-переменные, по умолчанию берите системную тему из prefers-color-scheme, выбор пользователя храните в localStorage и применяйте его маленьким скриптом в head до отрисовки страницы.
Короткий ответ
Правильная тёмная тема строится на трёх вещах:
- CSS-переменные (custom properties) для всех цветов — тема меняет значения переменных, а не сотни правил.
- prefers-color-scheme — по умолчанию сайт следует настройке операционной системы.
- Ручной переключатель с сохранением выбора и скриптом в
<head>, который применяет тему до первой отрисовки. Иначе пользователь увидит вспышку светлой темы.
Шаг 1. Цвета как токены
Не пишите color: #1a1033 в компонентах. Заведите семантические переменные: фон, текст, приглушённый текст, граница, акцент.
:root {
--bg: #ffffff;
--text: #1a1033;
--muted: #5f5873;
--border: #e4e0ee;
--accent: #7c3aed;
color-scheme: light;
}
:root[data-theme="dark"] {
--bg: #120b24;
--text: #ede9fe;
--muted: #a39cb8;
--border: #2c2343;
--accent: #a78bfa;
color-scheme: dark;
}
body {
background: var(--bg);
color: var(--text);
}
Свойство color-scheme подсказывает браузеру, что перекрашивать стандартные элементы: скроллбары, поля ввода, чекбоксы. Без него на тёмном фоне останутся светлые нативные контролы.
Шаг 2. Системная тема по умолчанию
Если пользователь ещё ничего не выбирал, уважайте настройку системы. Удобная схема: атрибут data-theme ставится только при ручном выборе, а без него работает медиазапрос.
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #120b24;
--text: #ede9fe;
/* остальные переменные тёмной темы */
color-scheme: dark;
}
}
Так получаются три состояния: системная, светлая, тёмная. Это честнее, чем двухпозиционный переключатель, который навсегда отключает следование системе.
Шаг 3. Переключатель с сохранением
function setTheme(theme) {
// theme: "light" | "dark" | "system"
const root = document.documentElement;
if (theme === "system") {
root.removeAttribute("data-theme");
} else {
root.setAttribute("data-theme", theme);
}
try {
localStorage.setItem("theme", theme);
} catch (e) {}
}
Обращение к localStorage заворачивайте в try/catch: в приватном режиме или при заблокированных cookies оно может выбросить ошибку.
Шаг 4. Убираем вспышку при загрузке
Если тема применяется после загрузки основного JavaScript, страница сначала рисуется светлой, а потом резко темнеет. Решение — крошечный блокирующий скрипт в <head> до стилей и контента:
<script>
try {
var t = localStorage.getItem("theme");
if (t === "light" || t === "dark") {
document.documentElement.setAttribute("data-theme", t);
}
} catch (e) {}
</script>
Он выполняется до первой отрисовки, поэтому вспышки нет. В React и Next.js учитывайте, что сервер не знает выбор пользователя: атрибут на <html> меняется до гидрации, и фреймворк может предупредить о несовпадении. Обычно на <html> для этого ставят suppressHydrationWarning или используют готовую библиотеку для тем.
Что часто делают неправильно
- Просто инвертируют цвета через
filter: invert(). Ломаются фото, логотипы и брендовые цвета. - Чистый чёрный фон и чисто белый текст. Слишком высокий контраст утомляет глаза; лучше глубокий тёмный оттенок и слегка приглушённый светлый текст.
- Забывают про тени. На тёмном фоне тени почти не видны — глубину лучше передавать более светлыми поверхностями.
- Не проверяют контраст. Приглушённый текст и акцентные кнопки часто проваливают требования WCAG в тёмной теме.
- Забывают картинки и графики. Иллюстрациям с белым фоном нужна отдельная версия или подложка, графикам — свои цвета.
Чек-лист перед релизом
- Все цвета в компонентах идут через переменные.
- Есть
color-schemeдля обеих тем. - Без ручного выбора сайт следует системе.
- Выбор сохраняется и применяется без вспышки.
- Контраст проверен для текста, ссылок, кнопок и состояний фокуса.
- Логотип, иконки и изображения читаются на обоих фонах.