Тёмная тема кажется простой функцией.

Берём светлый фон, делаем его тёмным. Чёрный текст меняем на белый. Добавляем переключатель — готово.

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

Тёмная тема — это отдельная цветовая система, а не фильтр invert().

Начинать нужно с переменных

Если цвета разбросаны по сотням CSS-правил, поддерживать две темы будет тяжело.

Удобнее заранее определить смысловые переменные:

:root {
  --bg: #ffffff;
  --surface: #f5f7fb;
  --text: #151922;
  --muted: #697386;
  --border: #dfe4ec;
  --accent: #5b5ce2;
}

Для тёмной темы меняются значения, а не структура компонентов:

[data-theme="dark"] {
  --bg: #07101d;
  --surface: #0d1828;
  --text: #f3f6fb;
  --muted: #9aa7b8;
  --border: #243247;
  --accent: #7b7dff;
}

Так кнопка, карточка или поле ввода продолжают использовать одни и те же переменные.

Не все тёмные цвета одинаковы

Чистый #000000 редко нужен как основной фон.

Если фон, карточка и поле ввода одинаково чёрные, интерфейс теряет глубину. Элементы приходится отделять яркими рамками, и страница начинает выглядеть тяжёлой.

Лучше использовать несколько близких уровней:

  • фон страницы;
  • поверхность карточки;
  • приподнятый блок;
  • активный элемент;
  • наведение.

Разница может быть небольшой, но она помогает глазу понимать структуру.

Контраст — не только белый на чёрном

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

Если весь текст чисто белый, интерфейс утомляет.

Полезно разделить:

  • основной текст;
  • приглушённое описание;
  • подписи;
  • неактивные элементы;
  • плейсхолдеры;
  • отключённые кнопки.

При этом нельзя делать вторичный текст слишком тёмным. На хорошем мониторе он может казаться нормальным, а на телефоне исчезнуть.

Что ломается чаще всего

Поля форм

У input, select и textarea есть системные состояния, автозаполнение и собственные стили браузера.

Особенно неприятно выглядит светлый фон автозаполненного поля внутри тёмной формы.

Бордеры

Рамка, которая хорошо работала на белом фоне, может полностью исчезнуть на тёмном.

И наоборот: слишком светлый бордер превращает каждую карточку в отдельную таблицу.

Иконки

Чёрная SVG-иконка может стать невидимой. Для интерфейсных иконок удобнее использовать currentColor.

Тени

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

Код и подсветка синтаксиса

Цвета кода должны проверяться отдельно. Красный, синий и фиолетовый на тёмном фоне могут иметь недостаточный контраст.

Переключатель темы

Выбор пользователя стоит сохранять локально:

localStorage.setItem("theme", "dark");

Но есть ещё системное предпочтение:

@media (prefers-color-scheme: dark) {
  /* начальная тема, если пользователь ещё ничего не выбрал */
}

Логика может быть такой:

  1. использовать явно сохранённый выбор;
  2. если выбора нет — учитывать тему системы;
  3. позволять переключить её вручную.

Как избежать вспышки светлой темы

Если класс темы добавляется только после полной загрузки JavaScript, тёмная страница может на мгновение появиться белой.

Решение — определить тему как можно раньше, небольшим скриптом в <head> до отрисовки основного интерфейса.

Этот скрипт должен быть коротким и не зависеть от остального приложения.

Проверять нужно не одну страницу

На AllBlog тема затрагивает разные типы интерфейса:

  • статьи;
  • карточки;
  • поиск;
  • галерею;
  • формы;
  • таблицы;
  • модальные окна;
  • онлайн-инструменты;
  • админку;
  • служебные страницы.

После изменения переменной полезно пройти весь сайт, а не только главную.

Именно на редко открываемой форме обычно остаётся тот самый белый блок.

Итог

Хорошая тёмная тема не должна ощущаться отдельной оболочкой.

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

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