Дизайн-токены и темизация: контракт между дизайном и кодом
Токены — трёхуровневый контракт: палитра-примитивы, семантика намерений, привязки компонентов. Тема переобъявляет семантический слой — data-theme переключает custom properties без ререндера. Версионируемый пакет, алиасы переименований, линт на hex; ребрендинг за дни, не месяцы.
Поглощение закрылось во вторник; мандат прилетел в четверг: новый бренд на каждой продуктовой поверхности за один квартал. Четыре веб-приложения: одно — на дисциплинированной трёхуровневой системе токенов, три — стилизованные как большинство приложений: hex-коды там, куда hex-коды упали. Токенизированное приложение закончило за шесть рабочих дней: слой палитры получил новые брендовые цвета, семантические маппинги отревьюили на требования контраста, тёмная тема приехала автоматически, потому что жила в семантическом слое, а продуктовый код не изменился вовсе — ни одного файла компонентов. Остальные три приложения потратили остаток квартала и сверх того: find-replace по тысячам файлов, неспособный отличить брендовый синий от идентичного синего в графике; четырнадцать подогнанных вручную приближений старого акцентного цвета, не совпадающих ни с одним поиском; тёмная тема, сломавшаяся дважды, потому что была реализована как инвертированные серые; и длинный хвост скриншотов от QA со смешанными старыми и новыми синими на одном экране. Одна компания, один ребрендинг, один квартал: шесть дней против четырёх месяцев. Разница была не в таланте инструментовки — а в том, существовал ли цвет как контракт или как десять тысяч совпадений.
Три уровня — или никакой темизации
Дизайн-токен — это именованное дизайн-решение: --color-accent, а не #2563eb. Именование и делает его контрактом: код ссылается на решение, и решение может меняться без ведома кода. Зрелые системы сходятся на трёх уровнях со строго однонаправленными ссылками:
- Палитра-примитивы — сырые значения без приклеенного смысла:
--blue-600: #2563eb,--gray-100,--space-4. Полный упорядоченный набор опций. Ничто в продуктовом коде не имеет права на них ссылаться. - Семантические токены — намерение:
--color-surface,--color-on-surface,--color-accent,--color-danger,--radius-interactive. Каждый отображается в примитив. Этот слой отвечает «для чего этот цвет» — и именно здесь живёт каждая тема. - Компонентные токены — покомпонентные привязки, потребляемые стилями компонента:
--button-primary-bg: var(--color-accent). Опциональны, но ценны в масштабе: дают каждому компоненту стабильный API стилизации и место для точечных переопределений.
Почему средний слой — тот, который нельзя пропустить: темы — это перепривязка намерений. Тёмная тема не говорит «сделай синий темнее» — она говорит «surface теперь почти чёрный, on-surface почти белый, accent на шаг светлее ради контраста». Если компонентные токены привязаны напрямую к примитивам — --button-bg: var(--blue-600) — выразить это утверждение негде, кроме как переобъявив каждый компонентный токен, сотни штук, на каждую тему. С семантическим слоем тема — это ~30 строк. А если продуктовый код использует примитивы напрямую — бренд вшит намертво, просто с красивыми именами: ребрендинг из Hook снова становится find-replace.
В системе токенов есть палитра-примитивы и покомпонентные токены, семантического слоя нет. Тёмную тему оценивают в недели по всему приложению, а не как один файл темы. Почему?
Рантайм: CSS custom properties против JS-объектов темы
Два лагеря реализации. CSS custom properties делают каскад браузера рантаймом темизации:
:root {
--blue-600: #2563eb; /* примитив — продукты на него не ссылаются */
--color-accent: var(--blue-600); /* семантика — тема живёт здесь */
--color-surface: #ffffff;
--color-on-surface: #111827;
}
[data-theme="dark"] {
--color-surface: #0b0f1a; /* перепривязка намерения; примитивы нетронуты */
--color-on-surface: #e5e7eb;
--color-accent: var(--blue-400); /* на шаг светлее ради контраста на тёмном */
}Переключение темы — это document.documentElement.dataset.theme = "dark": браузер пересчитывает стили, и никакого React-ререндера не происходит вовсе, потому что не изменилось никакое React-состояние. Это переживает SSR (инлайновый скрипт ставит атрибут до первой отрисовки, убивая вспышку темы), работает сквозь микрофронтенды и не-React-поверхности и не стоит ничего на компонент. Слабости реальны: значения — нетипизированные строки (лечится генерацией TypeScript-деклараций из источника токенов), а всё, что вычисляется из токена в JS, требует getComputedStyle.
JS-объекты темы — ThemeProvider, держащий { colors: { accent: ... } } и потребляемый через контекст, — дают типизированные, компонуемые значения прямо в языке. Счёт: тема связана с рантаймом — переключение меняет значение контекста, и оно обязано перерендерить каждого потребителя; в масштабе дизайн-системы это тысячи компонентов, перерисовывающихся ради цветов, которые браузер мог бы подменить в каскаде. Плюс привязка к одному фреймворку. Синтез, победивший на практике: писать токены как данные, типизировать на этапе сборки, доставлять как CSS custom properties — типизированное авторство, каскадный рантайм.
Одно правило тёмной темы стоит выше выбора реализации: темизируйте на семантическом слое, никогда — инверсией примитивов. Инвертированные серые шкалы дают нечитаемые disabled-состояния и сломанные тени; тёмным поверхностям нужно осветление по elevation (приподнятая карточка светлее страницы, а не темнее, потому что тени на тёмном почти невидимы); акцентным цветам часто нужен шаг светлее ради контраста. Это дизайн-решения на каждый токен намерения — что и есть весь аргумент за слой, в котором они живут.
Переключение темы реализовано как JS-объект темы в контексте, потребляемый ~1800 styled-компонентами; переключение заметно роняет кадры. Какова почти бесплатная альтернатива?
Дистрибуция: токены как версионируемый пакет
При более чем одном потребляющем приложении токены становятся API с потребителями — и требуют его дисциплины. Источник истины — файл данных (JSON), собираемый в платформенные выходы — CSS custom properties, TypeScript-модуль с типами, мобильные форматы — и публикуемый как версионируемый пакет, потребляемый как любая зависимость. Это включает политики, которые держат контракт доверенным:
- Политика breaking changes. Переименование токена — поломка API. Поставляйте алиасы устаревания — старое имя, реэкспортированное как ссылка на новое, с предупреждением на этапе сборки — минимум один мажор; удаляйте только на мажорном бампе, в идеале с codemod.
- Контроль дрейфа. Контракт разъедается по одному сырому hex за раз. Линтуйте: правило stylelint, запрещающее сырые цветовые литералы и px в продуктовом коде, форсируемое в CI. Токенизированное приложение из Hook уложилось в шесть дней потому что этот линт держал кодовую базу чистой; приложения с find-replace выплачивали годы незалинтованного дрейфа.
- Ревьюйте дифф токенов как дифф API. Изменённый семантический маппинг меняет каждого потребителя этого намерения; дифф мал для чтения и огромен по радиусу поражения — смена
--color-accentесть визуальное изменение всего продукта, ревьюируемое в одном файле.
Слой интеропа, честно
Токены переживают любой тулчейн, поэтому формат важен. Style Dictionary — устоявшийся трансформер: один источник токенов на входе, пер-платформенные выходы на выходе (CSS, TS, iOS, Android), с кастомными трансформами. Формат W3C Design Tokens Community Group (спека design-tokens/community-group на GitHub) — ставка на интероп: JSON-формат с $value/$type/$description, спроектированный, чтобы дизайн-инструменты, трансформеры и генераторы документации обменивались токенами. Честный статус: годы в драфте, поддержка инструментами реальна, но неровна — Style Dictionary v4 его читает, крупные дизайн-инструменты следуют с разной точностью, — и миграция существующего источника токенов на него — работа. Это всё равно правильная ставка, потому что альтернатива — пер-инструментный lock-in единственного артефакта, который ваши команды дизайна и кода обязаны делить годами.
▸Почему это работает
Почему компонентные токены заслуживают места третьим слоем, вместо прямого потребления семантики компонентами? Две причины, видимые только в масштабе. Первая — стабильный API стилизации на компонент: когда стили Button читают --button-primary-bg, продукт может перекрасить одно семейство кнопок — маркетинговый CTA, плотный вариант для data-инструмента — переобъявив три компонентных токена в скоупнутом селекторе, не трогая семантический слой, который всё ещё нужен остальной странице. Вторая — косвенность поглощает дизайнерскую болтанку: когда дизайн решает, что первичные кнопки должны взять новый акцент, изменение — один маппинг компонентного токена; потребители --color-accent в других местах (ссылки, кольца фокуса) не затронуты. Цена реальна — три слоя косвенности при отладке цвета, — поэтому маленькие системы легитимно живут на двух уровнях и добавляют третий, когда приходит давление переопределений и болтанки.
- 01Изложите три уровня токенов, направление ссылок и точную причину, по которой пропуск семантического слоя убивает темизацию.
- 02Сравните CSS custom properties и JS-объекты темы как рантаймы темизации и дайте дисциплину дистрибуции для мульти-приложенческих пакетов токенов.
Дизайн-токен — именованное дизайн-решение, и именование есть контракт: код ссылается на намерение, намерение отображается в значения, и значения меняются без ведома кода. Архитектура, делающая это реальным, — три уровня со ссылками строго вниз: палитра-примитивы из сырых бессмысленных значений, которых продуктовый код не касается; семантический слой токенов намерения, где живёт каждая тема; и компонентные токены, привязывающие намерение к стабильному API стилизации каждого компонента. Семантический слой — тот, который нельзя пропустить: тема есть перепривязка намерений, выразимая несколькими десятками переобъявлений — без среднего слоя она вырождается в переобъявление сотен компонентных привязок, а тёмная тема, построенная инверсией серых, ломается на тенях, elevation и контрасте, потому что тёмные темы сделаны из решений уровня намерений вроде поверхностей, светлеющих с подъёмом. В рантайме CSS custom properties бьют JS-объекты темы на самом переключении: переброс data-theme рестилизует через каскад с нулём React-ререндеров и без зависимости от гидратации, тогда как объект темы в контексте обязан перерендерить каждого потребителя, чтобы доставить новые значения, — типизируйте токены на сборке и отдайте рантайм каскаду. За пределами одного приложения токены — API с потребителями: один JSON-источник, собираемый в пер-платформенные выходы, версионируемый; переименования — алиасами устаревания с удалением только на мажорах; дрейф убит CI-линтом на сырые hex — дисциплина, превратившая корпоративный ребрендинг в шесть дней для токенизированного приложения, пока три его собрата жгли квартал на find-replace. Для интеропа честная ставка — Style Dictionary плюс формат W3C community group: несовершенный и в драфте, но лучше, чем запереть самый долгоживущий дизайн-артефакт в один инструмент. Теперь, когда услышишь «нам нужен ребрендинг за квартал», ты сможешь за минуты сказать, займёт ли это шесть дней или четыре месяца: открой любой файл продуктового компонента и посчитай сырые hex-коды.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.