Skip to content
PПромтбук
RUEN
02Дизайн-системы

Темизация дальше тёмной темы

Multi-theme: бренд-темы, density modes, accessibility modes. Стратегия токенов, переключение, что не масштабируется.

Действуй как architect design system. Спроектируй мульти-темизацию для {{product}} c набором: {{themes}}. Цель — не «добавить ещё одну тему», а архитектура, где тема — это axis, а не копипаста.

1. Какие axes темизации существуют

Не всё это «темы» — это разные оси:

  1. Цветовая схема: light / dark / sepia / amoled.
  2. Бренд: subbrand-A / subbrand-B / white-label tenant. Меняет accent + radius + шрифт.
  3. Density: comfortable / compact / spacious. Меняет spacing + size scale + line-height.
  4. Accessibility: high-contrast / large-text / reduced-motion. Меняет контрасты, размеры, анимации.
  5. Platform: ios / android / web. Меняет radius, ease, shadow.

Темы можно комбинировать: dark + brand-A + compact + high-contrast. Это 5 axes, не 5 тем.

2. Стратегия токенов (3 слоя)

Layer 1 (raw / primitive): --blue-500, --gray-900, --space-4
Layer 2 (semantic / alias): --color-fg, --color-bg-elevated, --space-card-padding
Layer 3 (component): --button-bg, --button-padding-x
  • Тема меняет ТОЛЬКО layer 2. Layer 1 — общая палитра, layer 3 — следует за layer 2.
  • Если компонент тянется в layer 1 (color: var(--blue-500)) — это баг, найди и переведи на семантику.

3. Реализация переключения

Через data-attributes (рекомендую)

<html data-theme="dark" data-brand="acme" data-density="compact" data-contrast="high">
:root { --color-fg: #111; --color-bg: #fff; }
[data-theme="dark"] { --color-fg: #f5f5f5; --color-bg: #0a0a0a; }
[data-brand="acme"] { --color-accent: #5b21b6; }
[data-density="compact"] { --space-card-padding: var(--space-2); }
[data-contrast="high"] { --color-fg: #000; --color-border: #000; }

Почему data-attributes а не класс: ортогональны, любую комбинацию легко выразить, CSP-дружественно, нет conflict resolution.

SSR + no-flash

  1. Прочти cookie / localStorage до первого рендера (Next.js: middleware или <script> в <head> синхронно).
  2. Установи data-theme на <html> до hydration.
  3. Никогда не делай useEffect(() => setTheme(...)) без guard — это даст flash of incorrect theme (FOIT).

Tailwind / CSS-in-JS

  • Tailwind v4: @variant dark (&:where([data-theme=dark], [data-theme=dark] *)).
  • В styled-components: тема через ThemeProvider, но всё равно используй CSS-переменные внутри — иначе run-time re-render всего дерева.

4. Что плохо масштабируется

  • Условия в JS-коде if (theme === 'dark') ... — превращает компонент в свалку. Условия живут в CSS-переменных, JS не должен знать о теме.
  • Дублирование стилей .btn-light / .btn-dark — каждая тема × каждый компонент = N × M, экспоненциальный рост.
  • Hard-coded hex в компонентах — единственный источник правды это токены layer 2.
  • Тема как JS-объект, прокинутый через props — невозможно SSR без mismatch, run-time только.
  • Темы зависят от компонента, а не от axes — добавление 6-й темы требует трогать все компоненты.

5. Tooling и QA

  • Visual regression по каждой комбинации тем для критичных компонентов (Chromatic / Playwright snapshots).
  • Tokens linter: запрет hex / rgb в файлах компонентов; разрешён только в tokens/.
  • Storybook: глобальный switcher для всех axes, render каждой story в light + dark + brand-X.
  • Contrast CI: автоматическая проверка каждой пары fg/bg в каждой теме.

Формат вывода

  1. Карта axes темизации для {{product}}.
  2. Структура папок: tokens/raw, tokens/semantic, tokens/themes/*.css.
  3. Контракт компонента: какие токены он обязан использовать.
  4. Switcher: код инициализации без FOIT.
  5. QA-чеклист: что проверяется в CI.

Anti-patterns (do NOT)

  • ❌ Дублирование CSS .btn-light / .btn-dark — растёт N×M.
  • if (theme === 'dark') в JSX компонентов — стили не должны знать про runtime.
  • ❌ Hard-coded #5b21b6 в компоненте — белый лейбл с другим брендом сломается.
  • ❌ Тема только через ThemeProvider без CSS-переменных — лишний re-render всего дерева на switch.
  • ❌ Темизация через классы вместо data-attributes — комбинации дают conflict resolution ад.
  • useEffect(() => document.body.classList.add('dark')) без SSR-инициализации — FOIT гарантирован.
  • ❌ Density через scale() / zoom — ломает hit-testing, тени, шрифты. Density меняется spacing-токенами.
  • ❌ High-contrast как «жирнее текст» — это не a11y. WCAG AAA = 7:1 contrast, фокус, размер touch-target.
  • ❌ Бренд-тема, которая меняет 50 токенов — это не тема, это форк. Тема трогает ≤ 10 семантических.
  • ❌ Темы вводятся «потому что красиво», без use-case — каждая тема стоит maintenance, добавляй по запросу.
К подразделу «Дизайн-системы»
Похожие промты