Как добавить тёмную тему (dark mode) в React с useState и useEffect

В последние годы тёмная тема стала стандартной опцией в пользовательских интерфейсах. Она помогает снизить яркость экрана в тёмных условиях, может уменьшать расход батареи на OLED‑экранах и часто воспринимается как более уютный визуальный стиль. Ниже показано несколько надёжных подходов для реализации dark mode в React, начиная с простого варианта и переходя к более зрелым архитектурам.
Краткое описание решения
Мы пройдём через эти шаги:
- реализуем минимальную версию с useState и useEffect
- добавим кнопку‑переключатель и CSS для двух тем
- обеспечим сохранение выбора в localStorage
- добавим поддержку prefers‑color‑scheme
- рассмотрим альтернативы: Context API, CSS-переменные, темы на уровне компонентов
- проверим критерии приёмки, тесты и рекомендации по доступности
Важно: если ваше приложение работает в корпоративной среде с политиками безопасности, уточните правила хранения данных при использовании localStorage.
Зачем нужна тёмная тема
Коротко по преимуществам:
- уменьшает яркость и потенциальное напряжение глаз в тёмных помещениях
- может сократить потребление энергии на OLED‑экранах
- улучшает восприятие контента при низкой освещённости
- даёт пользователям выбор и повышает удовлетворённость
Но тёмная тема не всегда подходит. В дневном свете тёмные интерфейсы иногда ухудшают читаемость, особенно при плохом цветовом контрасте.
Базовая реализация: useState + useEffect
Это минимально необходимый вариант для быстрого включения тёмной темы.
import React, { useState, useEffect } from 'react'
import './darkMode.css'
function App() {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'))
}
useEffect(() => {
document.body.className = theme
}, [theme])
return (
Пример приложения
)
}
export default AppФайл CSS (darkMode.css):
.dark {
background-color: #111;
color: #e6e6e6;
}
.light {
background-color: #ffffff;
color: #111111;
}
/* Базовая анимация для плавной смены темы */
body {
transition: background-color 200ms ease, color 200ms ease;
}Пояснение: мы назначаем класс на document.body, чтобы глобально применять стили. Такой подход прост и работает во многих случаях, но имеет ограничения при работе с изолированными компонентами и сторонними UI‑библиотеками.
Сохранение выбора пользователя в localStorage
Чтобы тема сохранялась после перезагрузки, используйте localStorage. Также полезно читать системную настройку как начальное значение.
import React, { useState, useEffect } from 'react'
import './darkMode.css'
function App() {
const getInitialTheme = () => {
const saved = localStorage.getItem('theme')
if (saved) return saved
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
return prefersDark ? 'dark' : 'light'
}
const [theme, setTheme] = useState(getInitialTheme)
const toggleTheme = () => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'))
}
useEffect(() => {
localStorage.setItem('theme', theme)
document.body.className = theme
}, [theme])
return (
Пример приложения с сохранением темы
)
}
export default AppПлюсы: пользовательский выбор сохраняется. Минусы: localStorage привязан к конкретному браузеру и извлекает данные только в том же origin, поэтому для синхронизации между устройствами потребуется серверная логика или согласование на аккаунтах.
Поддержка системной темы через prefers‑color‑scheme
CSS позволяет реагировать на системную тему без JavaScript:
@media (prefers-color-scheme: dark) {
:root {
--bg: #111;
--text: #e6e6e6;
}
}
@media (prefers-color-scheme: light) {
:root {
--bg: #ffffff;
--text: #111111;
}
}
body {
background-color: var(--bg);
color: var(--text);
}Этот способ удобен как запасной, но если вы хотите контролировать переключатель внутри приложения и сохранять выбор, комбинируйте его с JavaScript.
Более зрелые подходы и альтернативы
- Context API + custom hook
- Если тема должна быть доступна в глубине дерева компонентов, вынесите логику в ThemeProvider и useTheme hook. Это упрощает инжекцию темы в UI‑библиотеки и сервисы.
- CSS‑переменные для токенов темы
- Используйте CSS‑переменные для базовых цветов, отступов и т. п. Меняя переменные на уровне body, вы получаете адаптацию на стороне стилей без перестроения компонентов.
- UI‑библиотеки и design tokens
- Многие библиотеки (например, MUI, Chakra UI) имеют встроенные механизмы темизации. Рассмотрите миграцию на систему токенов для унификации стилей.
- Серверная и синхронизация между устройствами
- Для синхронизации темы между устройствами привязывайте настройку к аккаунту на сервере. Тогда при входе можно загружать предпочтение пользователя.
Практические советы по доступности и визуальному контрасту
- Проверяйте контраст текста и фона с помощью инструментов WCAG. Тёмный фон требует достаточной яркости текста для читаемости.
- Избегайте чистого чёрного и чистого белого в цветах интерфейса, используйте мягкие оттенки для уменьшения зрительной усталости.
- Убедитесь, что фокусируемые элементы и состояния (hover, active, focus) остаются хорошо видны в обеих темах.
Важно: автоматическое переключение темы при изменении системной настройки может нарушить ожидания пользователей, если они уже явно выбрали тему в приложении. Предпочтительнее сохранять явный выбор и предлагать опцию «следовать системной теме».
Критерии приёмки
- Переключатель темы переключает стили глобально и локально
- Выбранная тема сохраняется после перезагрузки
- В режиме prefers‑color‑scheme тема корректно применяется при отсутствии явного выбора
- Контраст соответствует рекомендациям WCAG для основных текстовых элементов
- Нет задержек или мерцания при инициализации темы
Тесты и случаи приёма
- Тест 1: при отсутствии localStorage и при system dark = true в initial theme открывается тёмная тема
- Тест 2: после клика по переключателю тема меняется и сохраняется в localStorage
- Тест 3: при смене системной темы и выключенном ручном контроле интерфейс реагирует через media query
- Тест 4: проверка таб‑навигации и видимости фокуса в обеих темах
Роль‑ориентированные чеклисты
Для фронтенд‑разработчика:
- реализовать переключатель и persist в localStorage
- использовать CSS‑переменные для основных токенов
- покрыть unit/e2e тестами переключение темы
Для дизайнера:
- подготовить палитру для light и dark с указанием цветов и контрастов
- указать состояния элементов и фокусные стили
Для QA:
- проверить на разных устройствах и браузерах
- прогнать автоматические проверки контраста
Миграционные заметки и совместимость
- Если приложение уже использует CSS‑in‑JS, интеграция темы через провайдер библиотеки часто проще, чем глобальные классы body
- Для серверного рендеринга определите начальную тему на сервере по cookies или по профилю пользователя, чтобы избежать визуального «мерцания» при гидратации
Примеры расширений и когда решение ломается
Когда простого применения className на body достаточно:
- небольшое SPA без сторонних библиотек
Когда подход может не подойти:
- крупные приложения с изолированными стилями (css modules, shadow DOM), где глобальный класс body не влияет на компоненты
- при необходимости синхронизации между устройствами без аккаунтов
Краткая методология внедрения (SOP)
- Добавить CSS‑переменные и две темы
- Реализовать toggle с useState и сохранением в localStorage
- Подключить prefers‑color‑scheme как запасной вариант
- Перенести общие токены в root и использовать в компонентах
- Добавить тесты и проверки контраста
- Документировать поведение для дизайнеров и QA
Короткий глоссарий
- prefers‑color‑scheme — CSS медиа‑фича, которая сообщает предпочтение системы по теме
- localStorage — браузерное хранилище для сохранения настроек на стороне клиента
- ThemeProvider — компонент для передачи темы через React Context
Итог
Добавление тёмной темы в React‑приложение можно реализовать быстро и безопасно с помощью useState и useEffect. Для продакшн‑решений рекомендуется учитывать сохранение выбора, поддержку prefers‑color‑scheme, доступность и архитектуру обмена темами через Context или design tokens. Начните с простого варианта и эволюционно переносите логику в ThemeProvider и CSS‑токены по мере роста приложения.
Примечание: при хранении пользовательских предпочтений в localStorage учтите особенности приватности и политики хранения данных в вашей организации.