ФронтендHTML / CSSJavaScript

Как сделать тёмную тему на сайте: CSS, переключатель и сохранение выбора

Добавляем тёмную тему на сайт через CSS-переменные: учитываем системную настройку, делаем доступный переключатель и сохраняем выбор пользователя.

Кодик

Автор

5 мин чтения

Надёжная тёмная тема начинается с CSS-переменных, а не с сотни новых классов. Вы заведёте палитру для двух режимов, учтёте системную тему, добавите одну кнопку и сохраните выбор в localStorage.

Если перекрашивать каждый элемент отдельно, новая карточка почти наверняка останется белой. Переменные превращают тему в небольшой набор токенов: фон, поверхность, текст, граница и акцент. Компоненты используют смысловые цвета и не знают, какая тема включена.

1Описываем

Пять смысловых цветов вместо случайных hex по файлу.

2Выбираем

Сначала сохранённое решение, затем системная настройка.

3Проверяем

Контраст, перезагрузка и смена темы без белой вспышки.

Как устроить палитру для двух тем

В корневом селекторе :root задайте светлые значения, а на html[data-theme="dark"] переопределите только переменные. Кнопка будет менять data-theme и aria-pressed. Компоненты продолжат использовать var(--surface), var(--text) и var(--border).

ТокенСветлая темаТёмная тема
--bg#f6f7fb#0f1221
--surface#ffffff#181c30
--text#171a2e#f4f6ff
--muted#62677f#aeb4cf
--accent#5b4bff#8b7cff

После этого шага у каждой части есть одна роль. Интерфейс собирает действие, логика меняет состояние, а вывод показывает признак успеха. Это проще отлаживать, чем один большой обработчик.

Как выбирается тема
Явный выбор важнее системного. Клик обновляет DOM и хранилище

Сначала соберите минимальный сценарий. Не добавляйте оформление, хранение и дополнительные режимы одновременно. Добейтесь одного правильного прохода от входных данных до результата, а затем проверяйте каждое улучшение отдельно.

Пишем CSS и переключатель на JavaScript

Не храните строку false в качестве темы. В localStorage достаточно значений light и dark. Если пользователь ещё ничего не выбирал, matchMedia подскажет системное предпочтение. aria-pressed показывает состояние кнопки вспомогательным технологиям.

<button id="theme" type="button" aria-pressed="false">Тёмная тема</button>
<main class="card"><h1>Панель проекта</h1><p>Тема меняется без перезагрузки.</p></main>
<style>
:root{color-scheme:light;--bg:#f6f7fb;--surface:#fff;--text:#171a2e;--muted:#62677f;--border:#dfe2ee;--accent:#5b4bff}
html[data-theme="dark"]{color-scheme:dark;--bg:#0f1221;--surface:#181c30;--text:#f4f6ff;--muted:#aeb4cf;--border:#303652;--accent:#8b7cff}
body{margin:0;background:var(--bg);color:var(--text);font:16px system-ui;transition:.2s}
.card{max-width:560px;margin:80px auto;padding:32px;background:var(--surface);border:1px solid var(--border);border-radius:24px}
button{margin:20px;padding:12px 16px;border:1px solid var(--border);background:var(--surface);color:var(--text);border-radius:12px}
@media(prefers-reduced-motion:reduce){body{transition:none}}
</style>
<script>
const button=document.querySelector('#theme');
const saved=localStorage.getItem('theme');
const systemDark=matchMedia('(prefers-color-scheme: dark)').matches;
let theme=saved ?? (systemDark?'dark':'light');
function apply(next){
  theme=next; document.documentElement.dataset.theme=theme;
  button.setAttribute('aria-pressed',String(theme==='dark'));
  button.textContent=theme==='dark'?'Светлая тема':'Тёмная тема';
}
apply(theme);
button.addEventListener('click',()=>{apply(theme==='dark'?'light':'dark');localStorage.setItem('theme',theme)});
</script>
Что должно работать после первого запуска

Компоненты используют смысловые CSS-переменные. Первый запуск учитывает prefers-color-scheme. Ручной выбор сохраняется после перезагрузки. Кнопка синхронизирует подпись и aria-pressed.

Цвета в проекте
Токены масштабируются лучше. Сравнение показывает, какой вариант легче поддерживать и проверять.

Как выбрать тему при первом запуске

color-scheme сообщает браузеру, какой вариант встроенных контролов и полосы прокрутки уместен. Это не красит ваши карточки автоматически, зато помогает нативным элементам не спорить с палитрой. data-theme остаётся вашим явным переключателем.

СитуацияИсточник темыРезультат
Есть localStorageСохранённое значениеВыбор пользователя важнее
Хранилище пустоprefers-color-schemeСовпадает с системой
Клик по кнопкеНовое значениеСохраняется
reduced motionМедиа-запросНет перехода

Сохраняйте в одном месте то, что может измениться. Если одно и то же значение скопировано в три функции, после первой правки они начнут расходиться. Один объект состояния и одна функция отрисовки снимают эту проблему.

Смотрите на состояние, а не только на красивый экран. Перезагрузка, повторный клик, пустой ввод и медленный ответ часто открывают ошибки, которые не видны в идеальном демонстрационном проходе.

Практика: проверяем контраст и сохранение

Проверьте не только фон и текст. Инпуты, границы, состояния hover, focus-visible, disabled и иллюстрации тоже должны читаться. Перезагрузите страницу после ручного выбора, очистите localStorage и поменяйте системную тему. Так вы увидите все три ветки логики. Отдельно откройте нативный select, поле даты и автозаполненный input: браузер рисует их часть сам, поэтому именно там часто остаётся чужой светлый фон или теряется текст. В конце сделайте снимки одного экрана в двух режимах и сравните визуальную иерархию: главный текст, вторичная подпись и активная кнопка должны сохранять одинаковую заметность.

Проверка своими руками
  1. Откройте страницу при светлой системной теме.
  2. Переключите режим и перезагрузите страницу.
  3. Удалите ключ theme из localStorage.
  4. Включите тёмную тему в системе и обновите страницу.
  5. Пройдите кнопку клавишей Tab.
  6. Сверьте текст и границы в обеих темах.
Готово, если выполняются все пункты

Нет белых карточек в тёмном режиме. Ручной выбор переживает перезагрузку. Без выбора работает системная тема. Текст, границы и фокус видны в обоих режимах.

Проверка двух тем
Четыре места, которые часто забывают. Пройдите все четыре сценария перед публикацией проекта.

Доводим тёмную тему до готового вида

Чтобы убрать короткую белую вспышку на реальном сайте, применяйте сохранённую тему маленьким скриптом в head до загрузки основного CSS. Не дублируйте всю таблицу стилей: меняйте токены и оставляйте компоненты общими.

Цвета зашиты в компоненты

Новая тема потребует переписать каждый блок.

Игнорируется система

Первый экран может ослепить пользователя.

Тема только в памяти

После F5 выбор исчезнет.

Серый текст слишком тусклый

Проверяйте контраст, а не только красоту палитры.

Сверьтесь с первичным источником. Поведение используемых функций и ограничения примера проверяйте по документацией MDN по prefers-color-scheme. Документация особенно важна, когда меняются версии, политика платформы или формат ответа API.

Что получится в итоге

Готовый результат проекта: Как сделать тёмную тему на сайте: CSS, переключатель и сохранение выбора
Готовая страница одинаково аккуратно выглядит в светлом и тёмном режиме, а переключатель запоминает выбор.

Короткие ответы
Зачем CSS-переменные?

Они отделяют смысл цвета от конкретного значения и позволяют менять палитру в одном месте.

Что важнее: система или сохранённый выбор?

Сохранённый явный выбор пользователя.

Для чего color-scheme?

Он помогает браузеру подобрать вид встроенных элементов под тему.

Как понять, что проект готов?

Он повторяет целевой сценарий несколько раз подряд, понятно реагирует на ошибочный ввод и не теряет состояние при обычных действиях.

Можно ли копировать код целиком?

Можно как стартовую точку, но сначала замените значения из примера, затем запустите проект по шагам. Если вы не можете объяснить одну строку, её лучше разобрать до следующего шага.

Соберите тему, которая не развалится на третьей карточке

Переменные, каскад и медиа-запросы разбираются в курсе CSS, события и localStorage в курсе JavaScript. Для структуры проекта откройте гайд по подключению JavaScript.

После темы добавьте адаптивное бургер-меню. В Кодике удобно открыть две версии рядом и проверить одни и те же состояния.