Перша дизайн-система, яку я побудував, вийшла з рівно однією помилкою, що переслідувала все подальше: ми використовували blue-500 прямо в коді компонентів. Кнопки були blue-500. Посилання були blue-500. Кільце фокусу було blue-500. Воно відчувалося чистим і DRY. Потім маркетинг провів ребрендинг із синього на бірюзовий, і ми витратили два спринти, ганяючись за кожним буквальним hex-посиланням у сорока репозиторіях — і все одно кілька пропустили, бо деякі були захардкоджені як #3B82F6, а не як змінна. Уся та епопея могла б бути зміною в один рядок. А не була вона нею саме з тієї причини, через яку взагалі існують колірні токени дизайн-системи.
Колірний токен — це просто іменоване посилання на значення кольору. Але цінність токенів не в найменуванні — вона в шарах найменування і в дисципліні щодо того, якого шару дозволено торкатися компоненту. Уловіть шарування правильно — і ребрендинг, запуск темної теми чи виправлення контрасту стають малою, локалізованою зміною. Уловіть неправильно — і ви отримаєте прибирання на сорок репозиторіїв.
Три шари: примітивний, семантичний, компонентний
Ментальна модель, що витримала перевірку в кожній системі, над якою я працював, — це трирівнева ієрархія. Кожен рівень посилається на той, що нижче, і компонентам дозволено споживати лише верхній рівень.
- Примітивні токени (їх також називають базовими чи глобальними) — це ваша сира палітра.
blue-500: #3B82F6,gray-900: #111827,red-600: #DC2626. Вони описують, чим колір є, і нічого про те, де його використовують. Примітив ніколи не має з’являтися прямо в CSS кнопки. - Семантичні токени описують намір — роботу, яку виконує колір.
color-text-primary,color-bg-surface,color-border-default,color-action-primary,color-feedback-error. Семантичний токен вказує на примітив:color-action-primary → blue-600. - Компонентні токени опціональні й описують конкретну частину конкретного компонента.
button-primary-bg,card-border-color,input-focus-ring. Вони вказують на семантичні токени, а не на примітиви.
Ось правило, що змушує все це працювати, і саме його більшість команд пропускає: компоненти посилаються на семантичні чи компонентні токени, ніколи на примітиви. Тієї миті, коли кнопка тягнеться повз семантичний шар, щоб ухопити blue-500 напряму, ви знову завели проблему на сорок репозиторіїв — по одному компоненту за раз.
Навіщо взагалі морочитися із семантичним середнім шаром? Бо це шов, де відбувається темізація. Коли ви перемикаєтеся в темну тему, ви не перефарбовуєте кожен компонент — ви перенацілюєте кілька десятків семантичних токенів на інші примітиви. Компоненти й гадки не мають, що щось змінилося. Ця непрямість — найважелніше рішення в усій системі.
Побудуйте шкалу, перш ніж щось називати
Перед семантикою вам потрібні примітиви, на які варто вказувати — а це означає числову шкалу, за конвенцією від 50 до 950. Конвенція, популяризована Tailwind (50, 100, 200 … 900, 950), фактично стала індустріальним дефолтом, і її варто прийняти бодай для того, щоб нові інженери почувалися як удома. 50 — найсвітліший відтінок, 500 — приблизно чистий брендовий колір, 950 — майже чорний.
Помилка, якої припускаються новачки, — генерувати шкалу наївним освітленням і затемненням у sRGB, додаючи білий для відтінків і чорний для затемнень. Ви отримуєте брудні півтони й кроки, що не відчуваються рівномірними, бо сприймана світлота нелінійна в RGB. 400 зрештою виглядає майже ідентично до 500, тоді як від 700 до 800 — урвище. Рішення — розставляти кроки в перцептивній моделі. Інструменти, що працюють у HSL, доведуть вас майже до кінця; інструменти, що використовують OKLCH (нині добре підтримуваний у CSS), доведуть до решти, бо OKLCH тримає сприйману світлоту узгодженою зі зміною відтінку. Якщо хочете швидко оцінити на око гармонійні відтінки й затемнення під час прототипування шкали, генератор колірної палітри — це швидкий спосіб побачити співвідношення між відтінками, перш ніж зафіксувати їх у токенах.
Кілька важко зароблених правил для шкал:
- Прив’язуйте шкалу до контрасту, а не до естетики. Вирішіть рано, який крок — ваш колір «текст на білому», і перевірте, що він влучає в потрібні коефіцієнти. У більшості нейтральних рамп саме на
600чи700ви перетинаєте 4.5:1 відносно білого. Знання цього дозволяє писати семантичні правила на кшталт «основний текст використовуєgray-700» упевнено. - Тримайте однакову кількість кроків у кожному відтінку. Якщо в синього є
950, а зелений зупиняється на900, ваші семантичні мапінги стають нерегулярними, а темна тема перетворюється на кошмар спеціальних випадків. - Не генеруйте забагато. Одинадцяти кроків на відтінок цілком досить. Я бачив команди, що випускали двадцятикрокові шкали, де половина кроків візуально нерозрізнювана, і ніхто ніколи не використовує
425.
Найменування: описуйте роботу, а не вигляд
Найважливіший принцип найменування такий: семантичні й компонентні токени мають називати призначення, ніколи вигляд. Токен color-text-red ламається того дня, коли текст помилки стає помаранчевим — назва тепер бреше. Токен color-feedback-error переживе цю зміну незайманим, бо «помилка» лишається правдою незалежно від того, у який червоний (чи помаранчевий) вона резолвиться.
Структура найменування, що масштабується, читається від широкої категорії до конкретного модифікатора, бо це чисто сортується й аудитується:
category-property-variant-statecolor-bg-surface,color-bg-surface-raised,color-bg-surface-sunkencolor-action-primary,color-action-primary-hover,color-action-dangercolor-text-primary,color-text-secondary,color-text-disabled,color-text-on-action
Останній — color-text-on-action — це токен, який люди забувають і про який шкодують. Коли ваш колір дії — насичений синій, текст на цьому синьому має бути білим чи майже білим, і ця пара не має нічого спільного з color-text-primary. Зробіть її явним токеном, інакше дивитиметеся, як інженери хардкодять #FFFFFF на кнопки, а потім заводять баги доступності, коли хтось вводить світло-жовту дію «попередження».
Для примітивів називати вигляд правильно й очікувано — blue-500 має описувати, чим він є, бо в цьому й увесь сенс шару примітивів. Правило «описуйте намір» застосовується вище нього.
Реалізація через CSS custom properties
CSS custom properties — природний дім для токенів, бо вони каскадуються й можуть бути перевизначені контекстом — а саме так і працює темізація. Патерн — два шари змінних: примітиви, визначені одного разу в корені, і семантика, визначена в корені й перевизначена під селектором теми.
Визначте примітиви глобально — --blue-600: #2563EB, --gray-900: #111827 тощо. Потім змапте семантику на них: --color-action-primary: var(--blue-600) і --color-text-primary: var(--gray-900). Компоненти читають лише семантичні змінні: color: var(--color-text-primary). Вони ніколи не називають примітив і ніколи не називають hex-код.
У Tailwind v4 це чисто лягає на нову директиву @theme, яка водночас оголошує токени й генерує з них утиліти, виставляючи кожен токен як рантаймову CSS-змінну. Ви визначаєте --color-text-primary: var(--color-gray-900) всередині @theme, використовуєте text-primary у розмітці й перевизначаєте змінну під селектором .dark у вашому базовому шарі. Офіційна документація Tailwind щодо змінних теми розкриває механіку, і та сама двошарова дисципліна застосовується незалежно від фреймворку.
Темна тема — це задача перенацілення токенів, а не задача кольору
Причина, чому семантичний шар відпрацьовує своє утримання, у тому, що темна тема стає майже тривіальною, щойно він існує. Ви не торкаєтеся жодного компонента. Ви перевизначаєте семантичні токени всередині області .dark (або [data-theme="dark"]), щоб вони резолвилися в інші примітиви.
У світлій темі --color-bg-surface: var(--white) і --color-text-primary: var(--gray-900). У темній темі, під селектором .dark, --color-bg-surface: var(--gray-900) і --color-text-primary: var(--gray-100). Кожен компонент, що читає color-bg-surface, перемикається автоматично, бо роботу робить каскад.
Дві речі, що тут кусають людей:
- Не просто інвертуйте шкалу. Чисто білий текст на чисто чорному (
#000на інвертованому#FFF) різкий і спричиняє ореол — той розмазаний відблиск навколо тексту на OLED-екранах. Темні поверхні мають бути темно-сірими, на кшталт#121212до#1A1A1A, а найяскравіший текст — м’яким майже-білим, а не#FFFFFF. Ось чому вашій шкалі потрібні справжні950і50, а не буквальні чорний і білий на кінцях. - Знесичуйте акценти для темних фонів.
blue-600, що виглядає впевнено на білому, може некомфортно вібрувати на майже-чорній поверхні. Темні теми зазвичай націлюють свій токен дії на світліший, трохи менш насичений крок —blue-400замістьblue-600. Оскільки це перенацілення одного токена, ви можете глобально підлаштувати його за секунди.
Пастки, що насправді ламаються на масштабі
Після достатньої кількості ребрендингів і запусків тем режими відмов римуються:
- Витік примітивів у компоненти. Первородний гріх. Лінтуйте на нього — правило, що забороняє назви примітивних токенів і сирий hex у файлах компонентів, варто написати в перший же день.
- Семантичні токени, що таємно кодують значення.
color-blue-action— це примітив у семантичному костюмі. Якщо назва містить відтінок, вона насправді не семантична й зламається на ребрендингу. - Пропуск семантичного шару «щоб рухатися швидше». Компоненти, прив’язані прямо до примітивів, відчуваються нормально, аж до вашої першої теми. Тоді ви рефакторите під тиском дедлайну — найдорожчий час, щоб це робити.
- Контраст, протестований лише у світлій темі. Пара, що проходить 4.5:1 на білому, може провалитися на темній поверхні, і навпаки. Кожна семантична пара текст-на-фоні потребує перевірки в кожній темі. Пороги WCAG — 4.5:1 для звичайного тексту і 3:1 для великого тексту та елементів UI, згідно з настановами W3C WCAG щодо контрасту — і ваша система токенів — це місце, де ви маєте закласти ці гарантії, а не місце, де ви маєте виявляти порушення.
- Забагато токенів. Система з чотирмастами семантичних токенів так само непридатна для використання, як і система з чотирма. Якщо два токени завжди резолвляться в те саме значення й завжди резолвитимуться, це один токен. Додавайте конкретику лише тоді, коли з’являється реальне розходження.
Чесний підсумок такий: токени не приймають за вас рішень про колір — вони роблять рішення про колір дешевими для зміни. Структура — це те, що купує вам можливість провести ребрендинг у п’ятницю, випустити темну тему за спринт і виправити баг контрасту в один рядок замість сорока репозиторіїв. Витрачайте зусилля на семантичний шар і найменування, тримайте компоненти чесними щодо того, якого рівня вони торкаються, — і система поглине зміни, яких ви ще навіть не можете передбачити.
Поширені запитання
У чому різниця між примітивними й семантичними колірними токенами?
Примітивний (або базовий) токен називає сире значення кольору без контексту — наприклад, blue-500: #3B82F6. Він описує, *чим* колір є. Семантичний токен називає *призначення* кольору, наприклад color-action-primary чи color-feedback-error, і вказує на примітив. Компоненти мають посилатися на семантичні токени, ніколи на примітиви, щоб ребрендинг чи темізація вимагали лише перенацілення семантичного шару, а не редагування кожного компонента.
Як називати колірні токени в дизайн-системі?
Називайте семантичні й компонентні токени за наміром, а не за виглядом. color-feedback-error переживе редизайн, де помилковий червоний стане помаранчевим; color-text-red — ні. Використовуйте структуру від загального до конкретного на кшталт category-property-variant-state (напр., color-bg-surface-raised, color-action-primary-hover). Примітивні токени — виняток: вони мають описувати буквальний колір, як gray-900, бо в цьому й полягає їхнє призначення.
Навіщо використовувати числову шкалу 50–950?
Шкала 50–950 (50, 100, 200 … 900, 950), популяризована Tailwind, стала індустріальним дефолтом, тож вона миттєво знайома новим інженерам. 50 — найсвітліший відтінок, ~500 — чистий брендовий колір, а 950 — майже чорний. Розставлення кроків у перцептивній моделі на кшталт HSL чи OKLCH — а не наївне змішування білого й чорного в sRGB — зберігає рівномірність кроків і уникає брудних півтонів.
Як колірні токени полегшують темну тему?
Темна тема стає задачею перенацілення токенів, а не редизайном. Оскільки компоненти читають лише семантичні токени, ви перевизначаєте ці токени всередині області .dark чи [data-theme=dark], щоб вони резолвилися в інші примітиви — наприклад, color-bg-surface вказує на білий у світлій темі й на gray-900 у темній. Каскад CSS автоматично перемикає кожен компонент, без змін у коді компонентів. Не забудьте знесичувати акцентні кольори й уникати чисто чорних фонів, щоб зменшити втому очей і ореол.
Яким коефіцієнтам контрасту мають відповідати колірні токени для доступності?
WCAG вимагає коефіцієнта контрасту щонайменше 4.5:1 для звичайного тексту і 3:1 для великого тексту та елементів UI. Закладіть ці перевірки у вашу систему токенів, валідуючи кожну семантичну пару текст-на-фоні в кожній темі. Комбінація, що проходить у світлій темі, може провалитися в темній, тож тестуйте обидві й визначте явний токен text-on-action, щоб підписи на насичених кнопках лишалися читабельними.
Хочете поекспериментувати з кольорами?
Спробуйте наш безкоштовний генератор кольорових палітр, щоб знайти ідеальну гармонію — із вбудованою перевіркою контрасту за WCAG.
Відкрити генератор