Більшість команд ламають темну тему на першому ж кроці: відкривають макет, вручну міняють #FFFFFF на #0E1219 і думають, що це «додати dark mode». Через тиждень з'ясовується, що половина карток лишилась білою, кнопки втратили контраст, а щоб оновити один відтінок сірого, треба пройти сорок екранів.
Темна тема — це не задача про кольори. Це задача про архітектуру токенів. Якщо структура правильна, перемикання Light ⇄ Dark — це один клік на фреймі, а не переробка макета. Розберемо архітектуру, яка масштабується, на прикладі змінних (Variables) Figma.
Головна помилка: дублювати компоненти замість режимів
Старий підхід — робити Button / Light і Button / Dark як окремі компоненти. Це подвоює бібліотеку, і кожну зміну (новий стан, новий розмір) доводиться робити двічі. Помилки в синхронізації неминучі.
Правильний підхід — один компонент, який реагує на режим (mode) колекції змінних. Кнопка прив'язана до семантичного токена bg/accent, а сам токен має різне значення в режимах Light і Dark. Перемикаєте режим на фреймі — усі прив'язані заливки оновлюються самі, без жодного оверрайду вручну.
Архітектура: дві колекції змінних
Робоча структура, яку рекомендує і сама Figma, і зрілі дизайн-системи, — це дві колекції:
- Primitives — сира палітра. Просто числа:
gray/900 = #0E1219,gray/50 = #F0F2F5,green/500 = #A8FF57. Без режимів. Це «фарба у банках». - Semantic — ролі інтерфейсу з двома режимами (Light / Dark). Наприклад
bg/surface,text/primary,border/subtle. Значення кожного токена — це посилання на примітив, і воно різне в кожному режимі.
Ключова ідея: тему перемикає тільки колекція Semantic. Примітиви не змінюються ніколи — вони однакові в обох темах. Змінюється лише те, на який примітив вказує семантична роль.
Правило одного рівня непрямості: компонент → семантичний токен → примітив. Компонент ніколи не бачить #hex і ніколи не бачить примітив напряму. Він знає лише роль — text/primary. Що це за колір сьогодні і чи темна зараз тема — не його турбота.
Крок 1: Primitives — палітра без режимів
Створіть колекцію Primitives з одним режимом. Занесіть повні шкали: нейтральні (gray/0…gray/1000), бренд, семантичні кольори статусів (red, amber, green для помилок/попереджень/успіху). Тут ви фіксуєте значення, а не сенс.
Порада: закладіть достатньо кроків у нейтральній шкалі. Темна тема майже завжди потребує проміжних відтінків, яких немає у світлій — поверхні на різній «висоті» (surface, surface-raised, surface-overlay) читаються саме через ці кроки.
Крок 2: Semantic — ролі з режимами Light і Dark
Створіть колекцію Semantic і додайте їй два режими: Light і Dark. Тепер для кожної ролі задайте значення в обох режимах — але значенням є alias на примітив, а не hex:
bg/surface → Light: gray/0 · Dark: gray/900
text/primary → Light: gray/900 · Dark: gray/50
text/subtle → Light: gray/600 · Dark: gray/400
border/subtle → Light: gray/200 · Dark: gray/800
bg/accent → Light: green/600 · Dark: green/500
Зверніть увагу на bg/accent: у темній темі акцент часто беруть світлішим, ніж у світлій, бо на темному тлі насичений колір «б'є» по очах. Це і є сенс семантичного шару — роль лишається тією самою, а значення підбирається під контекст.
Крок 3: Прив'язати компоненти лише до семантики
Пройдіться по бібліотеці й переконайтеся, що жоден шар компонента не тримає прямий hex і не тягне примітив напряму. Заливки, текст, обведення, тіні — усе прив'язане до токенів bg/*, text/*, border/*.
Після цього тест простий: киньте фрейм зі складним екраном, у панелі змінних перемкніть режим Semantic з Light на Dark. Якщо архітектура чиста — увесь екран перемкнеться коректно за один клік. Кожен елемент, що лишився не на місці, — це прямий hex, який ви пропустили. Це найшвидший спосіб знайти «діри».
Наміновка: описуй намір, а не колір
Найпоширеніша пастка семантичного шару — назви, прив'язані до кольору. text/gray перестає мати сенс тієї миті, коли в темній темі цей текст стає майже білим. Правильна назва описує роль, а не відтінок:
- ❌
text/gray,bg/white,border/light-gray - ✅
text/subtle,bg/surface,border/subtle
Тримайтеся патерну категорія/роль/варіант — text/primary, bg/surface-raised, border/focus. Він читається людиною і без перекладу лягає в код як CSS-змінні (--color-text-primary). Якщо у вас ще немає системи назв — почніть з нашого гайду по конвенціях найменування токенів, а логіку «спочатку примітиви, потім семантика» ми розбирали в окремій статті про примітиви.
Контраст: перевіряй у момент створення токена
Темна тема — не про «інвертувати кольори». Простий інверт майже завжди провалює контраст: те, що на світлому тлі мало 4.5:1, у темному може дати 3:1 і стати нечитабельним. Тому контраст треба закладати на етапі створення токена, а не ловити потім у макетах.
Практичне правило: для кожної пари «текст на тлі» перевіряйте співвідношення в обох режимах. text/primary на bg/surface має давати щонайменше 4.5:1 (звичайний текст, WCAG AA) і в Light, і в Dark. Великий текст і нетекстові елементи — від 3:1. Якщо пара не проходить у темному режимі — міняйте, на який примітив вказує роль, а не сам примітив.
Далі про доступність у Figma детально — у чеклисті WCAG для Figma-компонентів. Живий розбір архітектури токенів і темної теми ми також показуємо на YouTube-каналі UX Hero.
Висота поверхонь: чому в темній темі не працюють тіні
У світлій темі глибину показують тінню: чим вище елемент (модалка, дропдаун, тултип), тим м'якша й ширша тінь під ним. У темній темі тінь на майже чорному тлі не видно — вона просто зникає. Тому глибину передають світлістю поверхні: що вище елемент, то світліший його фон.
Це знову вирішується токенами, а не оверрайдами. Заведіть шкалу поверхонь як окремі семантичні ролі й прив'яжіть їх до різних кроків нейтральної шкали в темному режимі:
bg/surface → Dark: gray/900 (базове тло)
bg/surface-raised → Dark: gray/850 (картки)
bg/surface-overlay → Dark: gray/800 (модалки, поповери)
У світлому режимі ці ж три ролі можуть указувати на однаковий gray/0 плюс тінь — і це нормально. Сенс у тому, що компонент модалки прив'язаний до bg/surface-overlay завжди, а система сама вирішує, показати глибину світлістю (Dark) чи тінню (Light). Саме тому крокам нейтральної шкали з Кроку 1 варто приділити увагу заздалегідь: без проміжних відтінків передати висоту в темній темі нічим.
Що змінилося у 2026
Змінні Figma подорослішали, і кілька оновлень напряму спрощують теми:
- Більше режимів на колекцію. Тепер Light/Dark, бренд-варіації та навіть режими щільності (compact / comfortable) можуть жити в одній структурованій колекції, а не розповзатися по окремих компонентах.
- Нові типи змінних. Крім Color і Number з'явилися String (для лейблів і шляхів), Boolean (для тоглів і станів) та Composite — згруповані значення на кшталт тіней і рамок, що особливо зручно для темної теми, де тіні часто замінюють на світлі обведення.
Практичний висновок: якщо ви колись робили окремі компоненти під кожну тему — цей борг час закрити. Одна колекція з режимами замінює цілий пласт дублів.
Крок 4: Експорт у код без ручного синку
Правильна двоколекційна структура тому й цінна, що 1:1 лягає в код. Семантичні токени експортуються у форматі W3C DTCG, а звідти генеруються CSS-змінні під два скоупи теми:
:root { --color-bg-surface: #FFFFFF; --color-text-primary: #0E1219; }
[data-theme="dark"] { --color-bg-surface: #0E1219; --color-text-primary: #F0F2F5; }
Далі перемикання теми в застосунку — це один атрибут data-theme на <html>, а не переписування стилів. Імена змінних збігаються з іменами токенів у Figma, тож дизайн і код лишаються синхронними без «перекладу». Як тримати цей міст живим — розбирали у статті про CSS-змінні й design tokens.
Чеклист: коли темна тема готова
- Дві колекції: Primitives (без режимів) + Semantic (Light / Dark).
- Компоненти прив'язані лише до семантичних токенів — жодного hex і жодного примітива напряму.
- Назви описують роль (
text/subtle), а не колір (text/gray). - Кожна пара «текст/тло» проходить WCAG AA в обох режимах.
- Тест перемиканням: один клік по режиму коректно перефарбовує цілий екран.
- Токени експортуються в DTCG → CSS-змінні під
data-theme.
Темна тема, яку боляче підтримувати, — це майже завжди симптом плаского шару кольорів без семантики. Щойно з'являються дві колекції й один рівень непрямості, «додати dark mode» перестає бути проєктом і стає перемикачем. А наступна тема — brand, high-contrast, будь-яка — це просто ще один режим у тій самій колекції.