Тёмная тема (dark mode toggle)

Переключатель светлой/тёмной темы через CSS custom properties и класс data-theme на <html> — с сохранением в localStorage.

Задача

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

Решение

CSS — токены для двух тем:

/* Светлая тема (по умолчанию) */
:root {
  --color-bg: #ffffff;
  --color-text: #1e293b;
  --color-surface: #f8fafc;
  --color-border: #e2e8f0;
  --color-primary: #3b82f6;
}

/* Тёмная тема */
[data-theme="dark"] {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-surface: #1e293b;
  --color-border: #334155;
  --color-primary: #60a5fa;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  transition: background 0.3s, color 0.3s;
}

HTML:

<button class="theme-toggle" id="themeToggle" aria-label="Переключить тему">
  <span class="theme-toggle__icon" id="themeIcon">🌙</span>
</button>

JavaScript:

const html   = document.documentElement;
const toggle = document.getElementById('themeToggle');
const icon   = document.getElementById('themeIcon');

// Инициализация: localStorage > системные предпочтения > light
function getInitialTheme() {
  const saved = localStorage.getItem('theme');
  if (saved === 'dark' || saved === 'light') return saved;

  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  return prefersDark ? 'dark' : 'light';
}

function applyTheme(theme) {
  html.setAttribute('data-theme', theme);
  icon.textContent = theme === 'dark' ? '☀️' : '🌙';
  toggle.setAttribute('aria-label', theme === 'dark' ? 'Включить светлую тему' : 'Включить тёмную тему');
  localStorage.setItem('theme', theme);
}

// Применить сразу (до DOMContentLoaded)
applyTheme(getInitialTheme);

toggle.addEventListener('click', () => {
  const current = html.getAttribute('data-theme');
  applyTheme(current === 'dark' ? 'light' : 'dark');
});

// Реагировать на изменение системной темы
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  if (!localStorage.getItem('theme')) {
    applyTheme(e.matches ? 'dark' : 'light');
  }
});

Использование CSS-переменных в компонентах:

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text);
}

Ключевые моменты

  • data-theme на <html> — все CSS-переменные переопределяются каскадом для всего документа.
  • localStorage хранит выбор пользователя; matchMedia считывает системную тему как дефолт.
  • Применяй тему до загрузки CSS (<script> в <head>) — избежишь мигания FOUC (flash of unstyled content).
  • transition: background 0.3s на body — плавная смена темы без резкого переключения.

Варианты

  • CSS prefers-color-scheme без JS — автоматическая тема без переключателя:
    @media (prefers-color-scheme: dark) { :root { --color-bg: #0f172a; } }
    
  • Для React — useLocalStorage хук + Context для распространения темы.

Связанные рецепты / темы