Тематизация цвета в React: CSS-переменные и Context
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://colorfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://colorfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://colorfyi.com/entity//)
Use the native HTML custom element.
Тематизация — одна из тех задач, которые кажутся простыми, пока не приходится поддерживать её в масштабе. Единственный переключатель между светлым и тёмным режимом прост. Продукт, обслуживающий несколько брендов, предлагающий пользовательски настраиваемые палитры или требующий мгновенной смены темы без перезагрузки страницы, требует более обдуманной архитектуры.
Компонентная модель React и CSS-кастомные свойства — естественная пара для тематизации. CSS-переменные декларативно управляют цветовыми значениями; React Context управляет состоянием темы; Tailwind CSS (в современных проектах) объединяет оба. Это руководство охватывает каждый слой: как структурировать архитектуру темы, реализовать основу на CSS-переменных, подключить состояние React и обработать мультибрендовые сценарии.
Паттерны архитектуры тем
Паттерн 1: только CSS-переменные (без JavaScript-состояния)
Простейший паттерн для базовой поддержки тёмного режима не требует никакого состояния React. Медиазапрос prefers-color-scheme изменяет значения кастомных свойств, и каждый компонент обновляется автоматически:
/* globals.css */
:root {
--color-bg: #F8FAFC;
--color-text: #1E293B;
--color-brand: #2563EB;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0F172A;
--color-text: #F1F5F9;
--color-brand: #60A5FA;
}
}
Компоненты используют var(--color-bg) и никогда не нуждаются в знании текущей темы. Браузер берёт всё на себя.
Когда использовать: сайты, где вы хотите уважать системные настройки ОС и не нуждаетесь в управляемом пользователем переключателе.
Ограничение: нет возможности для пользователей переопределить системную настройку внутри приложения.
Паттерн 2: переключение темы через data-атрибут
Добавьте атрибут data-theme на элемент <html>, чтобы сделать активную тему явной и переопределяемой. Это совместимо с SSR, позволяет избежать вспышки неправильной темы и тривиально сочетается с сохранением в localStorage:
/* По умолчанию: светлая */
[data-theme="light"],
:root {
--color-bg: #F8FAFC;
--color-text: #1E293B;
--color-brand: #2563EB;
--color-surface: #FFFFFF;
--color-border: #E2E8F0;
}
/* Тёмная */
[data-theme="dark"] {
--color-bg: #0F172A;
--color-text: #F1F5F9;
--color-brand: #60A5FA;
--color-surface: #1E293B;
--color-border: #334155;
}
// Применяем тему до первой отрисовки (инлайн-скрипт в <head>)
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
Когда использовать: большинство продакшен-приложений, нуждающихся в управляемом пользователем переключателе. Подход с data-атрибутом — текущий индустриальный стандарт, используемый реализацией тёмного режима Tailwind CSS, Radix UI и большинством современных дизайн-систем.
Паттерн 3: переключение темы через классы
Аналогично data-атрибуту, но использует CSS-классы. На этом паттерне основана конфигурация darkMode: 'class' Tailwind:
.dark {
--color-bg: #0F172A;
--color-text: #F1F5F9;
}
Добавление класса dark к <html> активирует значения тёмных токенов. Tailwind затем применяет утилиты с префиксом dark:, когда этот класс присутствует.
Когда использовать: проекты, использующие Tailwind CSS со стратегией тёмного режима class.
CSS-переменные для цветов: система токенов
Семантическое именование вместо описательного
Самое важное решение в системе токенов — именование. Именование по цветовому значению (--blue-500, --gray-900) делает переменную понятной в изоляции, но неприменимой для тематизации — изменение --blue-500 на пурпурный разрушает семантику имени.
Именование по семантической роли (--color-brand, --color-text-muted) позволяет значениям полностью меняться между темами, сохраняя корректность компонентов:
:root {
/* ---- Примитивы цвета (не используются напрямую в компонентах) ---- */
--blue-500: #3B82F6;
--blue-700: #1D4ED8;
--slate-50: #F8FAFC;
--slate-800: #1E293B;
--slate-900: #0F172A;
/* ---- Семантические токены (используются в компонентах) ---- */
/* Текст */
--text-primary: var(--slate-800);
--text-secondary: #64748B;
--text-disabled: #94A3B8;
--text-inverse: var(--slate-50);
/* Поверхности */
--bg-base: var(--slate-50);
--bg-elevated: #FFFFFF;
--bg-sunken: #F1F5F9;
/* Бренд */
--brand: var(--blue-700);
--brand-hover: #1E40AF;
--brand-subtle: #EFF6FF;
--on-brand: #FFFFFF;
/* Обратная связь */
--error: #DC2626;
--error-bg: #FEF2F2;
--success: #16A34A;
--success-bg: #F0FDF4;
--warning: #D97706;
--warning-bg: #FFFBEB;
}
[data-theme="dark"] {
--text-primary: #F1F5F9;
--text-secondary: #94A3B8;
--text-disabled: #475569;
--text-inverse: #0F172A;
--bg-base: #0F172A;
--bg-elevated: #1E293B;
--bg-sunken: #0D1426;
--brand: #60A5FA;
--brand-hover: #93C5FD;
--brand-subtle: #172554;
--on-brand: #0F172A;
--error: #F87171;
--error-bg: #1C0A0A;
--success: #4ADE80;
--success-bg: #052E16;
--warning: #FCD34D;
--warning-bg: #1C1000;
}
Двухуровневая система (примитивы + семантические токены) даёт лучшее из обоих миров: сырую цветовую палитру для справки и семантические токены для использования в компонентах.
Генерация цветовой шкалы
Прежде чем строить систему токенов, необходима сырая цветовая палитра. Генератор оттенков создаёт полную шкалу 50–950 из единственного фирменного цвета — тот же шаблон шкалы, что используется в Tailwind CSS. Введите hex-код фирменного цвета и получите полный набор вариантов от тёмного к светлому, готовых к использованию в качестве примитивных токенов.
Например, введя #2563EB в качестве фирменного синего, генератор создаёт:
blue-50: #EFF6FFblue-100: #DBEAFEblue-500: #3B82F6blue-700: #1D4ED8blue-900: #1E3A8Ablue-950: #172554
Эти примитивные значения затем наполняют семантические токены, гарантируя, что цвета тёмного режима гармонически связаны со светлым режимом, а не являются произвольными тёмными приближениями.
React Context для состояния темы
Паттерн ThemeContext
Для приложений с управляемым пользователем переключателем темы React Context обеспечивает слой управления состоянием. Context хранит имя активной темы и предоставляет функцию переключения:
// contexts/ThemeContext.tsx
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextValue {
theme: Theme;
resolvedTheme: 'light' | 'dark'; // Фактически применённая тема (разрешает 'system')
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
if (typeof window === 'undefined') return 'system';
return (localStorage.getItem('theme') as Theme) || 'system';
});
const systemPrefersDark = typeof window !== 'undefined'
? window.matchMedia('(prefers-color-scheme: dark)').matches
: false;
const resolvedTheme: 'light' | 'dark' =
theme === 'system'
? (systemPrefersDark ? 'dark' : 'light')
: theme;
useEffect(() => {
const root = document.documentElement;
root.setAttribute('data-theme', resolvedTheme);
localStorage.setItem('theme', theme);
}, [theme, resolvedTheme]);
// Слушаем изменения системных настроек при theme === 'system'
useEffect(() => {
if (theme !== 'system') return;
const media = window.matchMedia('(prefers-color-scheme: dark)');
const handler = () => {
document.documentElement.setAttribute(
'data-theme',
media.matches ? 'dark' : 'light'
);
};
media.addEventListener('change', handler);
return () => media.removeEventListener('change', handler);
}, [theme]);
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
};
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}
ThemeProvider оборачивает ваше приложение (или соответствующее поддерево), а хук useTheme предоставляет доступ к состоянию темы из любого места в дереве компонентов:
// components/ThemeToggle.tsx
import { useTheme } from '../contexts/ThemeContext';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
style={{
background: 'var(--bg-elevated)',
color: 'var(--text-primary)',
border: '1px solid var(--color-border)',
padding: '8px 16px',
borderRadius: '8px',
cursor: 'pointer',
}}
>
{theme === 'dark' ? '☀ Light' : '☾ Dark'}
</button>
);
}
Компоненты используют CSS-кастомные свойства для визуальных стилей — им не нужно читать resolvedTheme для применения цветов. Context нужен только для UI, отображающего или управляющего текущим состоянием темы.
Предотвращение вспышки неправильной темы
Приложения с серверным рендерингом сталкиваются с проблемой вспышки неправильной темы: сервер рендерит HTML без знания пользовательских настроек темы, браузер кратко отображает этот HTML, затем React гидратируется и применяет правильную тему — вызывая видимую вспышку.
Решение — блокирующий инлайн-скрипт в <head>, читающий localStorage и устанавливающий атрибут темы до рендеринга страницы:
<!-- В вашем <head>, перед любыми ссылками на CSS -->
<script>
(function() {
try {
var stored = localStorage.getItem('theme');
var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var theme = stored === 'dark' || stored === 'light'
? stored
: (systemDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
} catch(e) {}
})();
</script>
В Next.js это помещается в _document.tsx или в корневой layout.tsx App Router. Поскольку скрипт выполняется синхронно до парсинга CSS, вспышки не возникает.
Tailwind CSS и темы
Tailwind v3: тёмный режим на основе классов
В Tailwind v3 тёмный режим требует установки darkMode: 'class' в tailwind.config.js. Это заставляет Tailwind применять утилиты dark: только при наличии класса .dark на элементе <html>:
// tailwind.config.js
module.exports = {
darkMode: 'class',
theme: {
extend: {
colors: {
brand: {
50: '#EFF6FF',
100: '#DBEAFE',
500: '#3B82F6',
600: '#2563EB',
700: '#1D4ED8',
900: '#1E3A8A',
950: '#172554',
},
},
},
},
};
Компоненты используют префиксы dark: для вариантов тёмного режима:
<div className="bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100">
<button className="bg-brand-600 dark:bg-brand-500 text-white hover:bg-brand-700 dark:hover:bg-brand-400">
Primary Action
</button>
</div>
Переключатель темы устанавливает класс .dark на document.documentElement — тот же подход, что и с data-атрибутом, только с классом вместо него.
Tailwind v4: CSS-ориентированная конфигурация
Tailwind v4 переносит конфигурацию целиком в CSS, нативно используя CSS-кастомные свойства:
/* styles.css */
@import "tailwindcss";
@theme {
--color-brand-50: #EFF6FF;
--color-brand-100: #DBEAFE;
--color-brand-500: #3B82F6;
--color-brand-600: #2563EB;
--color-brand-700: #1D4ED8;
}
Tailwind v4 генерирует утилитарные классы из этих кастомных свойств, и они сразу доступны в JSX: className="bg-brand-600 text-white". Конфигурация тёмного режима в v4 использует @variant dark:
@variant dark (&:where([data-theme="dark"] *)) {
/* Tailwind применяет утилиты dark: на основе этого селектора */
}
Этот вариант говорит Tailwind применять утилиты dark:, когда у предка есть data-theme="dark", интегрируясь нативно с паттерном data-атрибутов.
Стратегия мультибрендовой тематизации
Задача
SaaS-платформа, обслуживающая нескольких клиентов, white-label продукт или дизайн-система, используемая несколькими продуктовыми брендами, должна обрабатывать не только варианты тёмного/светлого режима, но и совершенно разные цветовые идентичности — каждая со своим основным брендовым цветом, акцентом, цветами статусов и нейтральными.
Брендовые токены как переопределения CSS-переменных
Наиболее поддерживаемый подход определяет брендово-нейтральные семантические токены в базовой таблице стилей, а затем переопределяет назначение примитивов для каждого бренда:
/* Базовая тема — одинаковые имена токенов для всех брендов */
:root {
/* Примитивы бренда — переопределяются для каждого бренда */
--brand-primary-raw: 37 99 235; /* #2563EB в каналах RGB */
--brand-accent-raw: 99 102 241; /* #6366F1 в каналах RGB */
/* Семантические токены — вычисляются из примитивов */
--brand-primary: rgb(var(--brand-primary-raw));
--brand-primary-hover: color-mix(in srgb, rgb(var(--brand-primary-raw)) 80%, black);
--brand-primary-subtle: color-mix(in srgb, rgb(var(--brand-primary-raw)) 10%, white);
}
/* Бренд: Acme Corp (синий) */
[data-brand="acme"] {
--brand-primary-raw: 37 99 235; /* #2563EB */
--brand-accent-raw: 16 185 129; /* #10B981 */
}
/* Бренд: Globex Inc (пурпурный) */
[data-brand="globex"] {
--brand-primary-raw: 124 58 237; /* #7C3AED */
--brand-accent-raw: 245 158 11; /* #F59E0B */
}
/* Бренд: Initech (зелёный) */
[data-brand="initech"] {
--brand-primary-raw: 22 163 74; /* #16A34A */
--brand-accent-raw: 239 68 68; /* #EF4444 */
}
Техника с сырыми каналами RGB (37 99 235) позволяет создавать варианты с прозрачностью без дополнительных токенов:
.overlay {
background-color: rgba(var(--brand-primary-raw), 0.1);
}
React Context для мультибрендовости
Расширьте контекст темы для обработки брендов:
// contexts/ThemeContext.tsx
type Brand = 'acme' | 'globex' | 'initech' | 'default';
type Theme = 'light' | 'dark';
interface ThemeContextValue {
theme: Theme;
brand: Brand;
setTheme: (theme: Theme) => void;
setBrand: (brand: Brand) => void;
}
export function ThemeProvider({ initialBrand = 'default', children }: {
initialBrand?: Brand;
children: React.ReactNode;
}) {
const [theme, setTheme] = useState<Theme>('light');
const [brand, setBrand] = useState<Brand>(initialBrand);
useEffect(() => {
const root = document.documentElement;
root.setAttribute('data-theme', theme);
root.setAttribute('data-brand', brand);
}, [theme, brand]);
return (
<ThemeContext.Provider value={{ theme, brand, setTheme, setBrand }}>
{children}
</ThemeContext.Provider>
);
}
Устанавливайте начальный бренд из слоя маршрутизации или аутентификации — субдомен каждого клиента или ID тенанта сопоставляется с идентификатором бренда.
Генерация брендовых шкал с помощью Генератора оттенков
Для каждого бренда необходима полная цветовая шкала, а не один-два hex-значения. Используйте Генератор оттенков для создания шкалы 50–950 для основного и акцентного цвета каждого бренда. Введите основной hex бренда и получите полный диапазон вариантов от тёмного к светлому.
Для Globex Inc с основным #7C3AED генератор создаёт полную пурпурную шкалу. Для Initech с #16A34A — полную зелёную шкалу. Эти шкалы затем наполняют примитивные токены для каждого бренда, гарантируя, что --brand-primary-subtle (светлейший тинт) и --brand-primary-hover (более тёмное нажатое состояние) остаются гармонически последовательными в рамках каждой брендовой идентичности.
Ключевые выводы
- CSS-кастомные свойства — основа: определяйте семантические токены (
--text-primary,--brand-primary) и изменяйте их значения для каждой темы, а не условно применяйте классы в каждом компоненте. - Переключение темы через data-атрибут (
data-theme="dark") — наиболее гибкий паттерн: он переопределяется JavaScript, совместим с SSR и сочетается с медиазапросамиprefers-color-scheme. - Двухуровневая система токенов (примитивная цветовая шкала + семантические токены ролей) обеспечивает полную гибкость темы при сохранении чистоты компонентов: примитивы определяют палитру, семантика — способ использования цветов.
- Используйте Генератор оттенков для создания полной цветовой шкалы 50–950 из фирменного основного цвета — это даёт полный диапазон светлых и тёмных вариантов, необходимых для полноценной системы токенов.
- React Context управляет состоянием темы (именем активной темы) и побочными эффектами (установка data-атрибута, сохранение в localStorage); компонентам никогда не нужно читать контекст для применения правильных цветов.
- Предотвращайте вспышку неправильной темы с помощью блокирующего инлайн-скрипта в
<head>, читающегоlocalStorageи устанавливающего атрибут темы до рендеринга браузером. - Интеграция Tailwind CSS: v3 использует
darkMode: 'class'с префиксамиdark:; v4 использует CSS-ориентированную конфигурацию@themeс нативной поддержкой CSS-переменных. - Мультибрендовая тематизация добавляет ещё один data-атрибут (
data-brand) поверх системы тем, переопределяя значения примитивных токенов для каждого бренда при сохранении семантических токенов и компонентов неизменными.