Гид по технологиям

Переключатель тёмной и светлой темы в React без Context

5 min read React Обновлено 18 Dec 2025
Переключатель тёмной/светлой темы в React
Переключатель тёмной/светлой темы в React

Фотография полумесяца на тёмном небе

Что потребуется

  • Node.js и базовый проект на React (create-react-app или аналог).
  • Знание React и хуков (useState, useEffect).
  • CSS-переменные для тем и возможность менять data-атрибут на body.

Коротко о подходе

Идея проста: компонент Button управляет состоянием theme (“light” или “dark”). При изменении темы мы записываем её в localStorage, устанавливаем data-theme на body, и тем самым активируем соответствующие CSS-переменные. При первом рендере мы берём приоритет: 1) локальное сохранённое предпочтение, 2) системная настройка через prefers-color-scheme, 3) значение по умолчанию.

Важно: такой компонент легко переносить между проектами, он не привязан к дереву компонентов через Context и минимально влияет на повторное использование.

Создание компонента переключения темы

Ниже — полный, исправленный и оптимизированный вариант Button.js. В нём учтены: корректное чтение/запись в localStorage, обработка prefers-color-scheme, доступность (aria-атрибуты) и предотвращение «мигания» при смене темы на клиенте.

import React, { useEffect, useState } from 'react';

const THEME_KEY = 'theme';
const DEFAULT_THEME = 'light';

function getStoredTheme() {
  try {
    return localStorage.getItem(THEME_KEY);
  } catch (e) {
    // Если доступ к localStorage невозможен — например, в режиме строгой приватности
    return null;
  }
}

function getSystemPreference() {
  if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
    return null;
  }
  const mql = window.matchMedia('(prefers-color-scheme: dark)');
  if (typeof mql.matches === 'boolean') {
    return mql.matches ? 'dark' : 'light';
  }
  return null;
}

export default function ThemeToggleButton() {
  const [theme, setTheme] = useState(() => {
    // Отложенная инициализация: сначала пробуем взять из localStorage
    const stored = getStoredTheme();
    if (stored) return stored;
    const system = getSystemPreference();
    return system || DEFAULT_THEME;
  });

  // Синхронизируем data-theme на body и localStorage при изменении theme
  useEffect(() => {
    try {
      document.body.dataset.theme = theme;
    } catch (e) {
      // Ничего не делаем, если доступ к body невозможен
    }
    try {
      localStorage.setItem(THEME_KEY, theme);
    } catch (e) {
      // Игнорируем ошибки записи (например, приватный режим)
    }
  }, [theme]);

  // Поддержка изменения системной настройки в реальном времени
  useEffect(() => {
    if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
    const mql = window.matchMedia('(prefers-color-scheme: dark)');
    const handler = (e) => {
      // Если пользователь явно не сохранил тему в localStorage, обновляем тему при смене системной
      if (!getStoredTheme()) {
        setTheme(e.matches ? 'dark' : 'light');
      }
    };
    if (typeof mql.addEventListener === 'function') {
      mql.addEventListener('change', handler);
    } else if (typeof mql.addListener === 'function') {
      mql.addListener(handler);
    }
    return () => {
      if (typeof mql.removeEventListener === 'function') {
        mql.removeEventListener('change', handler);
      } else if (typeof mql.removeListener === 'function') {
        mql.removeListener(handler);
      }
    };
  }, []);

  const toggle = () => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
  };

  return (
    
  );
}

Примечания по коду:

  • Отложенная инициализация useState позволяет избежать лишних эффектов при монтировании.
  • Обёрнуты вызовы localStorage и document.body для безопасной работы в нестандартных средах.
  • Добавлена подписка на изменение prefers-color-scheme, чтобы менять тему при смене системной настройки, если пользователь не зафиксировал собственную.
  • aria-атрибуты улучшают доступность для экранных читалок.

Подключение в App.js

Пример простого подключения компонента в корень приложения:

import React from 'react';
import ThemeToggleButton from './Button';

function App() {
  return (
    

Моё приложение

Контент страницы...

); } export default App;

CSS: переменные и темы

Приведу рекомендуемый набор CSS-переменных и стилей на основе data-атрибута body[data-theme]. Это простой и производительный способ менять тему — без перерендеринга React-компонентов.

:root {
  --color-text-primary: #131616;
  --color-text-secondary: #ff6b00;
  --color-bg-primary: #E6EDEE;
  --color-bg-secondary: rgba(125, 134, 136, 0.11);
  --transition-fast: 0.25s ease-in-out;
}

body {
  background: var(--color-bg-primary);
  color: var(--color-text-primary);
  transition: background var(--transition-fast), color var(--transition-fast);
}

body[data-theme="light"] {
  --color-text-primary: #131616;
  --color-bg-primary: #E6EDEE;
}

body[data-theme="dark"] {
  --color-text-primary: #F2F5F7;
  --color-bg-primary: #0E141B;
}

.themeBtn {
  padding: 10px 12px;
  color: var(--color-text-primary);
  background: transparent;
  border: 1px solid var(--color-text-primary);
  border-radius: 6px;
  cursor: pointer;
}

.themeBtn:focus {
  outline: 3px solid rgba(100, 150, 250, 0.35);
}

Совет: добавьте базовые переменные для дополнительных оттенков (фон карточек, бордюры), чтобы тема выглядела цельно.

Предотвращение «мигания» темы при первом рендере

Проблема: при SSR или при начальной загрузке тема может «мигнуть» (flash of wrong theme), если JS загружен позже, чем отображение страницы. Решения:

  • Inline-скрипт в , который читает localStorage и ставит data-theme до рендера. Простой пример (вставить в index.html):
  • Альтернативно, применяйте нейтральные цвета по умолчанию, которые выглядят хорошо в обеих темах, пока не инициализируется тема.

Доступность и UX

  • Используйте aria-pressed, aria-label, и видимое состояние фокуса.
  • Предоставьте явную надпись/иконку, понятную без цвета.
  • Не меняйте контраст текста ниже минимального уровня (WCAG 2.1 рекомендует контраст не ниже 4.5:1 для обычного текста).

Когда подход с data-атрибутом предпочтительнее Context, и когда нет

Когда data-атрибут лучше:

  • Хотите максимально простое решение для глобальных CSS-переменных.
  • Нужно мало кода и простая миграция между проектами.
  • Минимизировать перерендер компонентов.

Когда Context предпочтительнее:

  • Вам нужно, чтобы отдельные компоненты знали тему на уровне JS (например, для условного рендеринга сложных компонентов или передачи темы через props).
  • Нужно иметь сложную логику переключения, специфичную для набора компонентов.

Часто используют комбинированный подход: data-атрибут для стилей + Context для логики и доступа из компонентов.

Критерии приёмки

  • Кнопка меняет тему при клике и сохраняет выбор в localStorage.
  • При перезагрузке тема остается такой же, какой её выбрал пользователь.
  • При отсутствии сохранённой настройки тема соответствует системной настройке.
  • Кнопка доступна: aria-атрибуты, видно состояние фокуса, контраст в норме.
  • Отсутствует нежелательное мигание темы при инициализации (проверено в продакшене).

Тест-кейсы и приёмочные тесты

  1. Кликаем на кнопку: тема переключается и текст кнопки меняется.
  2. Сохраняем тему, перезагружаем страницу: тема сохраняется.
  3. Удаляем ключ theme из localStorage: тема соответствует системе (смена темы ОС меняет тему сайта).
  4. Браузер в приватном режиме, localStorage не доступен: приложение работает и не ломается.
  5. Проверить контраст текста для основных элементов в обеих темах.

Роль‑ориентированные чек-листы

Разработчик:

  • Добавил запись в localStorage и защитил её try/catch.
  • Подписался на prefers-color-scheme change.
  • Обновил документацию компонента.

Дизайнер:

  • Утверждены переменные цвета для обеих тем.
  • Проверены состояния hover/focus/disabled.

QA:

  • Выполнил тест-кейсы выше на основных браузерах и мобильных.
  • Проверил доступность с помощью экранного ридера.

Возможные альтернативы и расширения

  • Использовать Context для передачи темы в JS-код (если нужно условное рендеринг по теме).
  • Хранить выбор пользователя на сервере (при аутентификации) для синхронизации между устройствами.
  • Поддержка нескольких цветовых схем (не только light/dark) через тот же механизм data-theme.

Проблемы и когда подход не сработает

  • Если проект использует изолированные shadow DOM-компоненты, data-атрибут на body не влияет на их стили.
  • При SSR без inline-инициализации возможен flash of wrong theme.
  • В окружениях, где localStorage отключён, нужно быть готовым работать без сохранения.

Короткий глоссарий

  • prefers-color-scheme: медиа-фича, указывающая системную тему пользователя.
  • data-theme: data-атрибут на document.body, используемый для переключения CSS-переменных.
  • localStorage: браузерное хранилище для сохранения состояния на устройстве.

Резюме

  • Реализация через data-атрибуты проста, производительна и хорошо масштабируется по CSS.
  • Компонент с useState/useEffect и защитой localStorage покрывает большинство практических случаев.
  • Протестируйте поведение при приватном режиме, SSR и смене системной темы.

Важно: выберите стратегию, которая соответствует требованиям проекта — простота и переносимость против полной интеграции с бизнес-логикой через Context.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

RDP: полный гид по настройке и безопасности
Инфраструктура

RDP: полный гид по настройке и безопасности

Android как клавиатура и трекпад для Windows
Гайды

Android как клавиатура и трекпад для Windows

Советы и приёмы для работы с PDF
Документы

Советы и приёмы для работы с PDF

Calibration в Lightroom Classic: как и когда использовать
Фото

Calibration в Lightroom Classic: как и когда использовать

Отключить Siri Suggestions на iPhone
iOS

Отключить Siri Suggestions на iPhone

Рисование таблиц в Microsoft Word — руководство
Office

Рисование таблиц в Microsoft Word — руководство