Темизация в React

16 июня 2026
Автор

Олег Марков

Введение

Темизация — это способность приложения менять свой визуальный стиль централизованно, без изменения каждого компонента по отдельности. Самый известный пример — тёмный режим (dark mode), но темизация может включать и брендовые темы, тему для слабовидящих, сезонные темы и т.д.

В этой статье мы рассмотрим несколько подходов к темизации в React: от простого CSS-переменных решения до полноценных систем с TypeScript и централизованным управлением.

Подход 1: CSS Custom Properties (рекомендуемый)

Самый производительный и гибкий подход — CSS Custom Properties (переменные). Смена темы не требует перерендера React-компонентов.

Определение переменных

/* globals.css */
:root {
  /* Светлая тема (по умолчанию) */
  --color-background: #ffffff;
  --color-surface: #f7f8fc;
  --color-text-primary: #1a202c;
  --color-text-secondary: #718096;
  --color-border: #e2e8f0;
  
  --color-primary: #6c63ff;
  --color-primary-hover: #5a52d5;
  --color-primary-light: #f5f3ff;
  
  --color-success: #48bb78;
  --color-warning: #ed8936;
  --color-error: #e53e3e;
  
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 40px rgba(0, 0, 0, 0.15);
  
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 16px;
  
  --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-size-sm: 14px;
  --font-size-md: 16px;
  --font-size-lg: 18px;
  --font-size-xl: 24px;
}

/* Тёмная тема */
[data-theme="dark"] {
  --color-background: #1a202c;
  --color-surface: #2d3748;
  --color-text-primary: #f7fafc;
  --color-text-secondary: #a0aec0;
  --color-border: #4a5568;
  
  --color-primary: #8b85ff;
  --color-primary-hover: #7a74ee;
  --color-primary-light: #2d2a4a;
  
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 10px 40px rgba(0, 0, 0, 0.5);
}

Хук для управления темой

import { createContext, useContext, useEffect, useState } from 'react';

type Theme = 'light' | 'dark' | 'system';

interface ThemeContextValue {
  theme: Theme;
  resolvedTheme: 'light' | 'dark';
  setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setThemeState] = useState<Theme>(() => {
    // Читаем сохранённую тему из localStorage
    if (typeof window !== 'undefined') {
      return (localStorage.getItem('theme') as Theme) || 'system';
    }
    return 'system';
  });

  const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');

  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    
    const resolve = () => {
      if (theme === 'system') {
        return mediaQuery.matches ? 'dark' : 'light';
      }
      return theme as 'light' | 'dark';
    };

    const applyTheme = () => {
      const resolved = resolve();
      setResolvedTheme(resolved);
      document.documentElement.setAttribute('data-theme', resolved);
    };

    applyTheme();

    // Слушаем изменения системной темы
    if (theme === 'system') {
      mediaQuery.addEventListener('change', applyTheme);
      return () => mediaQuery.removeEventListener('change', applyTheme);
    }
  }, [theme]);

  const setTheme = (newTheme: Theme) => {
    localStorage.setItem('theme', newTheme);
    setThemeState(newTheme);
  };

  return (
    <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

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

import { useTheme } from '@/hooks/useTheme';

function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <div style={{ display: 'flex', gap: '8px' }}>
      <button
        onClick={() => setTheme('light')}
        style={{
          padding: '8px',
          borderRadius: '6px',
          border: 'none',
          background: theme === 'light' ? 'var(--color-primary)' : 'transparent',
          color: theme === 'light' ? 'white' : 'var(--color-text-primary)',
          cursor: 'pointer',
        }}
        title="Светлая тема"
      >
        ☀️
      </button>
      <button
        onClick={() => setTheme('dark')}
        style={{
          padding: '8px',
          borderRadius: '6px',
          border: 'none',
          background: theme === 'dark' ? 'var(--color-primary)' : 'transparent',
          color: theme === 'dark' ? 'white' : 'var(--color-text-primary)',
          cursor: 'pointer',
        }}
        title="Тёмная тема"
      >
        🌙
      </button>
      <button
        onClick={() => setTheme('system')}
        style={{
          padding: '8px',
          borderRadius: '6px',
          border: 'none',
          background: theme === 'system' ? 'var(--color-primary)' : 'transparent',
          color: theme === 'system' ? 'white' : 'var(--color-text-primary)',
          cursor: 'pointer',
        }}
        title="Системная тема"
      >
        💻
      </button>
    </div>
  );
}

Компоненты, использующие переменные

/* components.css */
.card {
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: 24px;
  box-shadow: var(--shadow-sm);
  transition: box-shadow 0.2s;
}

.card:hover {
  box-shadow: var(--shadow-md);
}

.button-primary {
  background-color: var(--color-primary);
  color: white;
  border: none;
  border-radius: var(--radius-md);
  padding: 10px 20px;
  font-size: var(--font-size-md);
  cursor: pointer;
  transition: background-color 0.2s;
}

.button-primary:hover {
  background-color: var(--color-primary-hover);
}

.text-primary {
  color: var(--color-text-primary);
}

.text-secondary {
  color: var(--color-text-secondary);
}

Подход 2: ThemeProvider из styled-components / Emotion

Если вы используете CSS-in-JS, ThemeProvider — нативный способ темизации:

import { ThemeProvider, DefaultTheme } from 'styled-components';

const lightTheme: DefaultTheme = {
  mode: 'light',
  colors: {
    background: '#ffffff',
    surface: '#f7f8fc',
    text: {
      primary: '#1a202c',
      secondary: '#718096',
    },
    primary: {
      main: '#6c63ff',
      hover: '#5a52d5',
      light: '#f5f3ff',
      contrastText: '#ffffff',
    },
    border: '#e2e8f0',
    status: {
      success: '#48bb78',
      warning: '#ed8936',
      error: '#e53e3e',
      info: '#4299e1',
    },
  },
  shadows: {
    sm: '0 1px 3px rgba(0, 0, 0, 0.05)',
    md: '0 4px 12px rgba(0, 0, 0, 0.1)',
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '48px',
  },
  borderRadius: {
    sm: '4px',
    md: '8px',
    lg: '16px',
  },
};

const darkTheme: DefaultTheme = {
  mode: 'dark',
  colors: {
    background: '#1a202c',
    surface: '#2d3748',
    text: {
      primary: '#f7fafc',
      secondary: '#a0aec0',
    },
    primary: {
      main: '#8b85ff',
      hover: '#7a74ee',
      light: '#2d2a4a',
      contrastText: '#ffffff',
    },
    border: '#4a5568',
    status: lightTheme.colors.status,
  },
  shadows: {
    sm: '0 1px 3px rgba(0, 0, 0, 0.3)',
    md: '0 4px 12px rgba(0, 0, 0, 0.4)',
  },
  spacing: lightTheme.spacing,
  borderRadius: lightTheme.borderRadius,
};

function App() {
  const { resolvedTheme } = useTheme();
  const theme = resolvedTheme === 'dark' ? darkTheme : lightTheme;

  return (
    <ThemeProvider theme={theme}>
      <Layout />
    </ThemeProvider>
  );
}

Компоненты используют тему через props.theme:

import styled from 'styled-components';

const Card = styled.div`
  background-color: ${p => p.theme.colors.surface};
  border: 1px solid ${p => p.theme.colors.border};
  border-radius: ${p => p.theme.borderRadius.lg};
  padding: ${p => p.theme.spacing.lg};
  box-shadow: ${p => p.theme.shadows.sm};
  transition: box-shadow 0.2s, background-color 0.3s;

  &:hover {
    box-shadow: ${p => p.theme.shadows.md};
  }
`;

const Button = styled.button`
  background-color: ${p => p.theme.colors.primary.main};
  color: ${p => p.theme.colors.primary.contrastText};
  border: none;
  border-radius: ${p => p.theme.borderRadius.md};
  padding: ${p => p.theme.spacing.sm} ${p => p.theme.spacing.md};
  cursor: pointer;
  transition: background-color 0.2s;

  &:hover {
    background-color: ${p => p.theme.colors.primary.hover};
  }
`;

Предотвращение мерцания при SSR

При серверном рендеринге (Next.js) может возникать мерцание (flash of unstyled content) при первой загрузке. Решение — инлайновый скрипт в <head>:

// app/layout.tsx (Next.js App Router)
export default function RootLayout({ children }) {
  return (
    <html lang="ru">
      <head>
        {/* Этот скрипт выполняется до парсинга HTML, предотвращая мерцание */}
        <script
          dangerouslySetInnerHTML={{
            __html: `
              (function() {
                try {
                  var theme = localStorage.getItem('theme');
                  var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
                  var resolved = theme === 'dark' || (!theme && systemDark) || (theme === 'system' && systemDark)
                    ? 'dark'
                    : 'light';
                  document.documentElement.setAttribute('data-theme', resolved);
                } catch(e) {}
              })();
            `,
          }}
        />
      </head>
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Многотемная система

Для продвинутых требований — несколько тем с возможностью выбора:

const themes = {
  light: { /* ... */ },
  dark: { /* ... */ },
  ocean: {
    mode: 'dark' as const,
    colors: {
      background: '#0d1b2a',
      surface: '#1b2a3b',
      text: { primary: '#e0f0ff', secondary: '#93b9d4' },
      primary: { main: '#00b4d8', hover: '#0096b7', light: '#0d2d3d', contrastText: '#fff' },
      border: '#1e3a4f',
      status: lightTheme.colors.status,
    },
    // ...
  },
  forest: {
    mode: 'dark' as const,
    colors: {
      background: '#0f1a10',
      surface: '#1a2b1b',
      text: { primary: '#e0ffe0', secondary: '#90b890' },
      primary: { main: '#4caf50', hover: '#43a047', light: '#1a2d1a', contrastText: '#fff' },
      border: '#1e3b1f',
      status: lightTheme.colors.status,
    },
    // ...
  },
};

type ThemeName = keyof typeof themes;

function ThemeSelector() {
  const [currentTheme, setCurrentTheme] = useState<ThemeName>('light');

  const themeLabels: Record<ThemeName, string> = {
    light: '☀️ Светлая',
    dark: '🌙 Тёмная',
    ocean: '🌊 Океан',
    forest: '🌿 Лес',
  };

  return (
    <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
      {(Object.keys(themes) as ThemeName[]).map(name => (
        <button
          key={name}
          onClick={() => setCurrentTheme(name)}
          style={{
            padding: '6px 12px',
            borderRadius: '20px',
            border: currentTheme === name ? '2px solid var(--color-primary)' : '2px solid transparent',
            background: currentTheme === name ? 'var(--color-primary-light)' : 'transparent',
            cursor: 'pointer',
            fontSize: '14px',
          }}
        >
          {themeLabels[name]}
        </button>
      ))}
    </div>
  );
}

Тема в Tailwind CSS

Для Tailwind темизация реализуется через class стратегию:

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        // Семантические цвета через CSS переменные
        background: 'var(--color-background)',
        surface: 'var(--color-surface)',
        primary: 'var(--color-primary)',
      },
    },
  },
};
// Tailwind автоматически применяет dark: классы
function Card({ children }) {
  return (
    <div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-xl p-6 shadow-sm dark:shadow-gray-900/30">
      {children}
    </div>
  );
}

Итоги

Система тем в React — важная часть современного UI. Выбор подхода зависит от стека:

Подход Когда использовать
CSS Custom Properties Любой проект, нет привязки к библиотеке
Styled Components ThemeProvider Проекты с Styled Components
Emotion ThemeProvider Проекты с Emotion/MUI
Tailwind dark: классы Проекты с Tailwind

Независимо от подхода, ключевые принципы одинаковы:

  • Храните тему в localStorage и синхронизируйте с системными настройками
  • Предотвращайте мерцание через инлайновый скрипт в <head>
  • Используйте семантические имена (--color-background, не --color-white)
  • Поддерживайте prefers-color-scheme для автоматического выбора темы
Стрелочка влевоXState - конечные автоматыТестирование хуковСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseCallback в React — мемоизация функцийuseDebugValue — отладка кастомных хуковuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий