Переключатель темы интерфейса
1) Принципы
1. Система > скин. Тема — не просто инверсия фона, а набор токенов (цвет, фон, контраст, тени, состояния, иллюстрации, графики).
2. System-first. По умолчанию — System (`prefers-color-scheme`) с возможностью вручную выбрать Light/Dark/High-Contrast.
3. Контраст по умолчанию. Цель — WCAG AA, для мелкого текста/важных меток — AAA.
4. Никаких вспышек. Применяем тему до рендера (inline-скрипт), а переходы делаем аккуратно.
5. Стабильность бренда. Акценты и семантика статусов сохраняются во всех темах.
2) Режимы и сценарии
Light — дневные сценарии/платежные формы/длинное чтение.
Dark — вечер/низкая освещенность/лайв-матчи; снижает блики.
System — следуем ОС/браузеру (`prefers-color-scheme`).
High-Contrast — повышенный контраст и минимизация украшений (вкл. reduce motion).
Seasonal/Promo (опционально) — поверх базовой темы под турнир/ивент (не ломает токены).
3) Архитектура токенов
Храним семантические токены, а не прямые цвета:css
:root {
/ semantics/
--bg: hsl(0 0% 99%);
--bg-elev: hsl(0 0% 100%);
--fg: hsl(220 15% 15%);
--muted: hsl(220 10% 45%);
--accent: hsl (260 95% 60%) ;/brand accent/
--success: hsl(152 55% 40%);
--warning: hsl(36 85% 45%);
--danger: hsl(0 75% 50%);
--border: hsl(220 10% 90%);
--focus: hsl(260 95% 60% /.6);
--shadow: 0 6px 24px hsl(220 20% 10% /.08);
/ typography/radii/
--radius: 12px;
--lh: 1. 4;
}
:root[data-theme="dark"]{
--bg: hsl(220 18% 10%);
--bg-elev: hsl(220 18% 14%);
--fg: hsl(0 0% 96%);
--muted: hsl(220 10% 70%);
--accent: hsl (260 95% 65%) ;/slightly lighter in the dark/
--border: hsl(220 14% 22%);
--shadow: 0 8px 28px hsl(220 50% 2% /.6);
}
:root[data-theme="hc"]{
/ high-contrast: simple pairs, high Lc/
--bg: #000; --bg-elev:#000; --fg:#fff; --muted:#fff;
--accent:#00E; --success:#0F0; --warning:#FF0; --danger:#F00;
--border:#fff; --focus:#0FF;
}
Правило: компоненты используют только семантические токены.
4) Детектор и сохранение выбора
html
<script>
(function(){
const saved = localStorage. getItem('theme'); // 'light' 'dark' 'hc' 'system' null const sys = window. matchMedia('(prefers-color-scheme: dark)'). matches? 'dark': 'light';
const theme = saved && saved!=='system'? saved: sys;
document. documentElement. setAttribute('data-theme', theme);
})();
</script>
UI-переключатель: `Light / Dark / System / High-Contrast`. При выборе `System` не храните конкретный цвет, только флаг. Слушайте изменения ОС:
js matchMedia('(prefers-color-scheme: dark)'). addEventListener('change', e=>{
if(localStorage. getItem('theme')==='system'){
document. documentElement. setAttribute('data-theme', e. matches? 'dark': 'light');
}
});
5) Плавные переходы без FOUC
Применяйте тему до загрузки CSS (inline-скрипт).
Анимации темы — короткие и только `color/background/border-color` (120–200 мс), но не при первом рендере:css
@media (prefers-reduced-motion: no-preference){
html. theme-ready { transition: color. 18s, background-color. 18s, border-color. 18s; }
}
После монтирования приложения добавьте `class="theme-ready"`.
6) Компоненты и состояния
Текст/иконки: контраст ≥ AA; вторичный текст не ниже 4.5:1 (в дарке легко «выцветает»).
Поля/карточки: фон `--bg-elev`, граница `--border`, тень `--shadow`.
CTA: фон `--accent`, текст контрастный (`#fff` или вычисляемый).
Состояния (hover/focus/active/disabled): изменяйте яркость/альфу, а не «переливайте радугу».
Графики/спарклайны: отдельные палитры для light/dark; сетка/оси низкоконтрастные, но читабельные.
7) Изображения/медиа/логотипы
Иконки монохромные — через `currentColor` (подстраиваются под текст).
Логотипы брендов не инвертируйте; готовьте две версии (light/dark).
Постеры/скриншоты: легкий overlay в дарке (8–12%) для читабельности текстов.
SVG: избегайте «жестких» заливок, используйте варс `var(--fg)`/`var(--accent)`.
8) Доступность
Высокий контраст: отдельный пресет `data-theme="hc"`.
Фокус-кольца: всегда видимы (`outline: 2px solid var(--focus); outline-offset: 2px`).
Не полагайтесь на цвет. Иконка/текст/паттерн для статусов.
Шрифты: `font-variant-numeric: tabular-nums;` для сумм/коэффов.
RTL: тема не ломает зеркалирование (используем логические свойства).
9) Перформанс
Цвета — CSS custom properties на корне → мгновенное переиспользование без ререндера компонентов.
Избегайте «перекраски» изображений фильтрами `invert()` на больших контейнерах.
Ленивая подмена тяжелых иллюстраций для темы (если нужны).
Не храните большие палитры в JS — тема управляется классом/атрибутом.
10) Специфика iGaming
Live-коэффы ночью: «мягкий» контраст (AAA для чисел), подсветка изменения коэффициента — ненавязчивая, без мерцаний.
Ответственная игра: напоминания и подсказки читабельны в обеих темах; без «ядовитых» цветов ночью.
Касса: поля/подписи/ошибки без утомляющих светящихся акцентов; успех/ошибка стабильны по теме.
Турнирные «скины»: только как поверхностные accent-override, не ломайте базовые токены.
11) Сниппеты UI
Переключатель (HTML/JS):html
<label class="theme-switch">
<span> Topic </span>
<select id="theme">
<option value = "system "> System </option>
<option value = "light "> Bright </option>
<option value = "dark "> Dark </option>
<option value = "hc"> High contrast </option>
</select>
</label>
<script>
const sel = document. getElementById('theme');
sel. value = localStorage. getItem('theme') 'system';
sel. addEventListener('change', e=>{
const v = e. target. value;
localStorage. setItem('theme', v);
if(v==='system'){
const sys = matchMedia('(prefers-color-scheme: dark)'). matches? 'dark':'light';
document. documentElement. setAttribute('data-theme', sys);
} else {
document. documentElement. setAttribute('data-theme', v);
}
});
</script>
Компонентные пресеты:
css
.btn{height:44px; padding:0 16px; border-radius:var(--radius); display:inline-flex; align-items:center; gap:8px}
.btn--primary{background:var(--accent); color:#fff}
.card{background:var(--bg-elev); border:1px solid var(--border); box-shadow:var(--shadow); border-radius:var(--radius)}
.text-muted{color:var(--muted)}
React hook (persist + system):
tsx import { useEffect, useState } from 'react';
type Theme = 'light' 'dark' 'hc' 'system';
export function useTheme(){
const [theme, setTheme] = useState<Theme>(()=>localStorage. getItem('theme') as Theme 'system');
useEffect(()=>{
const apply = (t:Theme)=>{
const v = t==='system'? (matchMedia('(prefers-color-scheme: dark)'). matches? 'dark':'light'): t;
document. documentElement. setAttribute('data-theme', v);
};
apply(theme);
const mql = matchMedia('(prefers-color-scheme: dark)');
const on = ()=> theme==='system' && apply('system');
mql. addEventListener('change', on);
return ()=> mql. removeEventListener('change', on);
},[theme]);
useEffect(()=> localStorage. setItem('theme', theme),[theme]);
return { theme, setTheme };
}
12) Метрики
Theme adoption rate: доля пользователей на Dark/System/HC.
FOUC rate: доля с видимым «белым всплеском» при старте (<1%).
Contrast defects: количество багов контраста по релизу.
Error visibility: клики/повторы из-за «незаметных» ошибок в разных темах.
Energy impact (мобайл): сравнение времени сессии в дарке vs лайте (косвенная метрика).
13) Анти-паттерны
Инвертировать все `filter: invert(1)` — ломает бренд и смыслы.
Менять цвета напрямую в компонентах без токенов.
Прятать фокус-кольца в темной теме.
Слишком темный текст на темном фоне (или светлый на светлом).
Длинные transition на всю страницу (подтормаживания).
«Эксклюзивные» цвета статусов в одной теме, которых нет в другой.
14) QA-чек-лист
Контраст и читаемость
- Все тексты ≥ AA; критичные метки/мелкий текст ≥ AAA.
- Ошибки/успех/предупреждения различимы не только цветом.
Поведение
- System уважает `prefers-color-scheme` и реагирует на смену ОС.
- Нет FOUC (тема применяется до рендера).
- Переключение темы не сбрасывает состояние страниц.
Компоненты
- Карточки/формы/таблицы используют только токены.
- Графики имеют палитры для обеих тем.
- Логотипы/иконки корректно видны в обеих темах.
A11y
- Видимый focus-ring; High-Contrast режим доступен.
- Учитывается `prefers-reduced-motion`.
Перформанс
- Переход ≤ 200 мс; без глобальных reflow.
- Нет тяжелых фильтров/блендов на контейнерах.
15) Документация в дизайн-системе
Theme tokens: палитры, контрасты, состояние (hover/focus/active/disabled).
Guides: как добавлять новый брендо-акцент без регресса контраста.
Charts/Media: преднастроенные палитры для light/dark.
Patterns: System-first, High-Contrast, плавное переключение без FOUC.
Do/Don’t: инверсия фильтром, inline-цвета, невидимые ошибки/фокус.
Краткое резюме
Рабочий переключатель темы — это семантические токены + System-first + безмерцательный старт. Фиксируйте контраст, централизуйте цвета, уважайте `prefers-color-scheme` и reduce-motion, храните выбор пользователя и избегайте тяжелых эффектов. Тогда UI остается читабельным и узнаваемым в любой обстановке — от ночного лайв-матча до дневной кассы и турнирных экранов.