Більшість команд ламають темну тему на першому ж кроці: відкривають макет, вручну міняють #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, будь-яка — це просто ще один режим у тій самій колекції.