Тёмная тема за один день — если ты правильно настроил переменные заранее
Основной чат
Чат для вайбкодеров: новости, гайды, поиск исполнителей, маркетплейс и разбор реальных кейсов.
Есть два способа добавить тёмную тему. Первый: открываешь каждый компонент, меняешь цвета вручную, смотришь как половина рассыпается, переделываешь три дня, всё равно что-то пропускаешь. Второй: создаёшь Dark Mode в Figma Variables, задаёшь тёмные значения, переключаешь — всё обновляется за секунду. Один день вместо трёх.
Разница — в одном решении принятом в начале: использовать семантические токены или нет.
Тёмная тема это не инверсия
Самая распространённая ошибка: сделать тёмную тему как механически инвертированные цвета светлой. Результат выглядит странно — и это не случайность.
Оптическая вибрация. Насыщенные цвета на тёмном фоне вибрируют — это физиологический эффект. #E94560 на белом фоне выглядит энергично. Тот же цвет на #1A1A2E — агрессивно и дискомфортно. Акценты в тёмной теме нужно делать менее насыщенными или чуть светлее.
Тени исчезают. box-shadow: 0 4px 12px rgba(0,0,0,0.1) — тёмная тень на светлом фоне создаёт глубину. На тёмном фоне она почти невидима. В тёмной теме elevation показывается через светлость поверхности: то что «выше» — светлее.
Contrast ratio меняется. Цвет который проходит WCAG AA 4.5:1 в светлой теме может не проходить в тёмной. Особенно серые оттенки. После добавления тёмной темы — отдельная проверка каждого текстового элемента.
Архитектура Variables: Light и Dark Mode
Figma Variables, Collections, два Mode: Light и Dark. Каждый компонент использует семантический токен — не конкретный цвет. При переключении Mode меняются значения под теми же именами.
Коллекция: Color System
Modes: Light | Dark
color/background/page:
Light → #FFFFFF
Dark → #0F0F1A ← темнее чем surface
color/background/surface:
Light → #F5F5F0
Dark → #1A1A2E
color/background/elevated:
Light → #FFFFFF (+ тень)
Dark → #252540 ← светлее surface, elevation через яркость
color/text/primary:
Light → #1A1A2E
Dark → #F5F5F0
color/text/secondary:
Light → #4A4A6A
Dark → #9090B0
color/text/disabled:
Light → #AAAACC
Dark → #505070
color/brand/accent:
Light → #E94560
Dark → #D4304A ← чуть темнее, убирает вибрацию
color/border/default:
Light → #E0E0F0
Dark → rgba(255,255,255,0.1)
color/status/error:
Light → #E94560
Dark → #FF6080 ← чуть светлее для видимости на тёмном
Elevation через светлость, не тени
В тёмной теме карточка должна «подниматься» над фоном страницы — но тени не работают. Решение: каждый уровень elevation немного светлее.
:root[data-theme="dark"] {
--color-background-page: #0F0F1A; /* самый тёмный */
--color-background-surface: #1A1A2E;
--color-background-elevated: #252540; /* карточки, модали */
--color-background-overlay: #303060; /* тултипы, dropdown */
}
.card {
background: var(--color-background-elevated);
border: 1px solid rgba(255, 255, 255, 0.08);
/* box-shadow убирается в тёмной теме */
}
Компоненты которые всегда ломаются
Инпуты. Placeholder теряет контраст на тёмном фоне — часто не проходит 3:1. Border в idle-состоянии почти невидим. Проверяй оба.
PNG с прозрачностью. Логотипы с белыми деталями на тёмном фоне выглядят странно. Варианты: SVG который стилизуется через CSS (fill: currentColor), или <picture> с отдельной версией для тёмной темы.
Графики. Цвета Chart.js/Recharts обычно захардкожены в конфиге. Нужен хук:
function useChartColors() {
const style = getComputedStyle(document.documentElement)
return {
primary: style.getPropertyValue('--color-chart-primary').trim(),
secondary: style.getPropertyValue('--color-chart-secondary').trim(),
grid: style.getPropertyValue('--color-chart-grid').trim(),
}
}
Иконки с захардкоженным fill. fill: #1A1A2E невидима на тёмном фоне. fill: currentColor работает автоматически. Проведи отдельный аудит иконок.
Модальный оверлей. rgba(0,0,0,0.5) на тёмном фоне почти не даёт разделения с контентом. Нужно rgba(0,0,0,0.7) или backdrop-filter: blur(4px).
CSS: переключение без вспышки
:root {
--color-background-page: #FFFFFF;
--color-background-surface: #F5F5F0;
--color-text-primary: #1A1A2E;
--color-brand-accent: #E94560;
--color-border-default: #E0E0F0;
}
/* Системный режим (без ручного переключения) */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-background-page: #0F0F1A;
--color-background-surface: #1A1A2E;
--color-text-primary: #F5F5F0;
--color-brand-accent: #D4304A;
--color-border-default: rgba(255,255,255,0.1);
}
}
/* Ручное переключение */
[data-theme="dark"] {
--color-background-page: #0F0F1A;
--color-background-surface: #1A1A2E;
--color-text-primary: #F5F5F0;
--color-brand-accent: #D4304A;
--color-border-default: rgba(255,255,255,0.1);
}
/* Плавный переход — ТОЛЬКО цвета, не всё */
*, *::before, *::after {
transition: background-color 0.2s ease, color 0.15s ease,
border-color 0.15s ease;
}
transition: all сломает существующие анимации. Только color-свойства.
Когда начинать: до или после запуска
Если продукт ещё не запущен — строй с тёмной темой сразу. При правильной архитектуре токенов это почти бесплатно: добавляешь Dark Mode в Variables и задаёшь значения.
Если продукт уже запущен без токенов — сначала переводи на Variables, потом добавляй тёмную тему. Пытаться добавить тёмную тему без предварительного перевода на переменные — это переделка каждого компонента дважды. Дороже в два раза.
Тёмная тема — хорошая причина навести порядок в системе: чтобы она заработала, всё должно быть на семантических токенах. Захардкоженные значения вылезут сразу. Это не проблема — это шанс зафиксировать долг и разобраться с ним.
Промпт для Codex / Claude Code
Добавь поддержку тёмной темы в проект.
Токены тёмной темы:
- background/page: #0F0F1A
- background/surface: #1A1A2E
- background/elevated: #252540
- text/primary: #F5F5F0
- text/secondary: #9090B0
- brand/accent: #D4304A (НЕ #E94560 — вибрирует на тёмном)
- border/default: rgba(255,255,255,0.1)
- status/error: #FF6080
Требования:
1. Все значения через CSS Custom Properties (нет захардкоженных hex)
2. Поддержка prefers-color-scheme И ручного [data-theme="dark"]
3. Кнопка переключения в хедере, сохранение выбора в localStorage
4. В тёмной теме: elevation через светлость поверхностей, не box-shadow
5. transition только для color-свойств (не transition: all)
После добавления:
- Проверь contrast ratio text/primary на каждом фоне (цель ≥ 4.5:1)
- Замени box-shadow карточек на border: 1px solid rgba(255,255,255,0.08)
- Найди иконки с захардкоженным fill и замени на currentColor
- Проверь placeholder в инпутах (contrast ≥ 3:1)
Как тёмная тема ломает вещи которые казались надёжными
Опыт команд которые добавляли тёмную тему без подготовки: сначала кажется что всё работает. Потом начинают появляться баги.
Самый неожиданный: кастомные иконки нарисованные в Figma и экспортированные как SVG с захардкоженным fill="#1A1A2E". В светлой теме — тёмные иконки на светлом фоне. В тёмной теме — тёмные иконки на тёмном фоне. Невидимы. И никто не думал об этом при создании.
Второй неожиданный: emails и PDF которые генерируются из продукта. Они используют светлые цвета потому что печатаются или смотрятся в email-клиентах которые не поддерживают тёмный режим. Токены здесь не помогают — нужны отдельные правила для этих контекстов.
Третий: скриншоты и превью которые генерирует продукт (превью ссылок в чатах, thumbnails). Они обычно рендерятся в headless браузере который игнорирует пользовательские предпочтения. Нужно явно задавать светлую тему для этих сценариев.
Всё это не делает тёмную тему неосуществимой. Просто нужно проверить все контексты в которых используется продукт — не только основное приложение.
Тёмная тема как момент для проверки accessibility
По статистике WCAG проверок, большинство accessibility-проблем с контрастом обнаруживается именно при добавлении тёмной темы. Потому что в светлой теме команда привыкла к конкретным цветам и не проверяет их — они «и так работают».
При добавлении тёмной темы нужно проверить каждую комбинацию фон/текст. Хороший инструмент для этого: плагин Stark в Figma — он проверяет контраст прямо в Figma-файле при переключении на Dark Mode. Chrome DevTools также показывает contrast ratio при инспекции текстовых элементов.
Минимальный набор что проверять: основной текст на каждом фоне (page, surface, elevated), вторичный текст, placeholder в инпутах, текст в кнопках всех вариантов, текст статусных сообщений (error, success, warning).
Частые вопросы про тёмную тему
«Нужно ли поддерживать тёмную тему если у нас мало пользователей на мобайле?» Тёмная тема нужна не только мобайлу. Десктоп-пользователи на macOS и Windows также используют системный тёмный режим. По данным 2025–2026, около 55% пользователей десктопа включили тёмную тему в системных настройках.
«Нужно ли тестировать каждый экран в тёмной теме?» Да. Но не каждый компонент — они тестируются в Storybook. Каждый экран нужно просмотреть визуально при переключении в Figma Dark Mode, и затем верифицировать в браузере с [data-theme="dark"].
«Что делать с пользователями у которых старый браузер без поддержки prefers-color-scheme?» Для них работает светлая тема по умолчанию. Тёмная тема через prefers-color-scheme — прогрессивное улучшение. Ручное переключение через [data-theme] работает везде где работает JavaScript — то есть в 99.5% браузеров.
«Как тёмная тема влияет на производительность?» CSS Custom Properties переключаются мгновенно — один атрибут на :root. Никакой дополнительной загрузки, никакого мерцания (если правильно настроен transition). Производительность идентична светлой теме.
☐ Все компоненты на Variables (нет захардкоженного hex)
☐ Dark Mode создан в Figma Variables
☐ Акценты в тёмной теме: менее насыщены
☐ Elevation: карточки светлее фона, без box-shadow
☐ Контраст текста ≥ 4.5:1 в тёмной теме
☐ Placeholder в инпутах ≥ 3:1
☐ PNG с прозрачностью проверены
☐ Иконки: fill: currentColor вместо захардкоженного цвета
☐ Переключение без вспышки (transition только color-свойства)