Архитектура фронтенда
Дизайн-токены: единый источник истины до любого ребрендинга
Маркетинг утверждает ребрендинг: фирменный синий меняется с #0052CC на более тёплый #1A66FF. Оценка — «день». Уходит одиннадцать недель. Hex захардкожен в 340 web-компонентах, в Swift-расширении UIColor, в Android colors.xml, в двух email-шаблонах и в Figma-библиотеке, которую никто не может синхронизировать. Grep находит 18 разных написаний «одного и того же» синего — #0052cc, #0052CC, rgb(0,82,204), rgba(0, 82, 204, 1). У тёмной темы свои форкнутые копии. Три из них пропускают, и в прод уезжает двухцветная кнопка.
Токен — это решение с именем, а не значение с именем
Дизайн-токен — это именованная запись, хранящая дизайн-решение: цвет, отступ, радиус, размер шрифта, — так, что всё ниже по цепочке ссылается на имя, а не на сырое значение. Смысл не в том, чтобы дать #0052CC псевдоним blue600; само по себе это ничего не экономит. Смысл в том, что имя несёт намерение, а намерение — это то, что ты меняешь при ребрендинге, переключении темы или фиксе доступности.
Провал из хука — это то, что бывает без токенов: сырое значение скопировано в каждого потребителя, поэтому значение и есть источник истины, размноженный сотни раз. Нет единой вещи, которую можно отредактировать. Токен переворачивает это — одно определение, много ссылок, — тот же ход «единый источник истины», что и с производным состоянием, только применённый к дизайн-решениям, а не к рантайм-состоянию.
Примитив, семантика, компонент: три уровня
Наивная версия — дать каждому hex имя и остановиться — ломается в момент добавления тёмной темы, потому что blue600 — неправильная вещь, на которую ссылаться из кнопки. Тебе не нужно «кнопка использует синий 600». Тебе нужно «кнопка использует интерактивный цвет», и он мапится в синий 600 в светлой теме и во что-то ярче в тёмной. Фикс — три уровня, каждый ссылается на нижний:
- Примитивные (глобальные) токены — сырая палитра:
color.blue.600 = #0052CC,space.4 = 16px. Без смысла, просто варианты. Компонент никогда не должен ссылаться на них напрямую. - Семантические токены (алиасы) — решения с намерением:
color.interactive.default = {color.blue.600},color.surface.base,color.text.primary. Это слой, который переопределяют темы. - Компонентные токены — самые специфичные:
button.background = {color.interactive.default}. Опциональны, но позволяют одному компоненту отклониться, не трогая семантический слой.
| Уровень | Пример | Несёт | Кто ссылается |
|---|---|---|---|
| Примитив | color.blue.600 = #0052CC | Сырое значение, без смысла | Только семантические токены |
| Семантика | color.interactive.default = {color.blue.600} | Намерение / роль | Компоненты и темы |
| Компонент | button.background = {color.interactive.default} | Привязка одного компонента | Один компонент |
Почему это работает
Признак того, что в системе нет семантического уровня: grep по var(--color-blue-600) находит попадания внутри стилей кнопки, ссылки и таба. Это значит, что ребрендинг или тёмная тема обязаны тронуть каждое из них, а палитра протекла в продуктовый код. Компоненты, ссылающиеся на примитивы, — самая частая причина, по которой «токенизированная» система всё равно не может ребрендиться дёшево.
Пайплайн: один источник, много платформ
Токены пишутся один раз — всё чаще в формате W3C Design Tokens Community Group (DTCG), который достиг первой стабильной версии v2025.10 в октябре 2025. Это обычный JSON: каждый токен — объект с $value и $type, алиасы используют синтаксис ссылки {group.token}, файл имеет расширение .tokens.json и media type application/design-tokens+json. Композитные типы (типографика, тень, граница, градиент) объединяют несколько подзначений в один токен.
Этот JSON — единый источник. Билд-инструмент — Style Dictionary это эталонная реализация — читает его и трансформирует под каждую платформу. Трансформы — несущая часть: один и тот же токен space.4 становится --space-4: 1rem; для CSS (px→rem), space_4 (snake_case) в Android colors.xml и spaceFour (camelCase) в Swift. Цвет становится #0052CC для CSS, но UIColor(red:green:blue:alpha:) для iOS и 8-значным ARGB-hex (#FF0052CC, alpha первой) для Android. Одно определение токена, три корректных вывода под платформы — сгенерированных, а не скопированных руками. Этого шага генерации как раз и не хватало в ребрендинге-из-ада: билда не было, поэтому каждая платформа была ручным форком.
Расставь пайплайн дизайн-токенов от написания до стилизованной кнопки:
- 1 Написать токены один раз в DTCG JSON ($value, $type, ссылки {alias})
- 2 Style Dictionary читает источник и резолвит все алиасы
- 3 Запускаются трансформы под платформы (px→rem, hex→UIColor, name→snake_case)
- 4 Форматтеры выдают CSS custom properties, colors.xml, Swift-файл
- 5 Кнопка ссылается на --button-background → интерактивный цвет → синий 600
Темы и тёмный режим: переопределяй семантический слой, а не hex
На web вывод — это CSS custom properties, и здесь каскад делает работу бесплатно. Ты определяешь семантические токены на :root, затем переопределяешь те же семантические имена под селектором темы — и каскад пере-резолвит всё, что на них ссылается, мгновенно и без JS:
:root {
--color-blue-600: #0052CC; /* примитив */
--color-interactive: var(--color-blue-600); /* семантика */
}
[data-theme="dark"] {
--color-blue-600: #4D8AFF; /* то же имя, ярче значение */
}
.button { background: var(--color-interactive); } /* не трогаем */Кнопка вообще не упоминает тёмную тему. Переключи data-theme — и каждый компонент, читающий --color-interactive, перекрашивается за одну перерисовку, потому что custom properties каскадируются и наследуются как любое другое свойство. Дисциплина сеньора: компонент обязан ссылаться на семантический токен, никогда на примитив и никогда на литерал. В момент, когда кнопка хардкодит #0052CC или даже var(--color-blue-600), она выпадает из темы — и ты заново создал тот форк, который тёмная тема должна была убрать.
Поэтому же самое дешёвое принуждение — это lint-правило. stylelint-plugin-no-raw-colors (или похожее) делает захардкоженный hex вне файла примитивов ошибкой билда. Без него система эродирует за недели: кто-то в спешке вставляет hex, оно работает, оно уезжает в прод, и drift начинается заново. Токены — это соглашение, а соглашения без гейта деградируют.
Кнопка должна автоматически перекрашиваться в тёмной теме. На что должен ссылаться её CSS?
Трейдофф, который взвешивает сеньор: сколько уровней и как скоро
Три уровня не бесплатны. Каждый слой косвенности — это хоп, который новый инженер обязан проследить (button.background → color.interactive → color.blue.600), и место, где может спрятаться ошибка. Для продукта на пять человек с одной темой и без нативных приложений полная трёхуровневая система с билд-пайплайном — это оверинжиниринг: двух уровней (примитив + семантика) как CSS-переменных вполне достаточно, а компонентные токены ты добавляешь только когда компоненту действительно надо отклониться.
Расчёт резко меняется в момент, когда у тебя больше одного целевого вывода — web плюс iOS плюс Android, или светлая плюс тёмная плюс высококонтрастная, или white-label продукт с темами под каждого клиента. Каждая новая цель умножает цену отсутствия пайплайна: площадь drift растёт как платформы × темы. Прочтение сеньора — масштабировать систему под число целей, в которые ты реально отгружаешь, и вводить пайплайн до того, как приземлится вторая цель, а не после того, как ребрендинг выставит счёт на одиннадцать недель.
Продукт отгружается на web, iOS и Android, со светлой и тёмной темами, и на следующий квартал запланирован ребрендинг. Выбери стратегию токенов.
- 01Почему «токенизированная» система, которая даёт каждому hex имя, всё равно не может ребрендиться дёшево, и чего не хватает?
- 02Как один и тот же токен оказывается корректным на web, iOS и Android без того, чтобы кто-то копировал значения руками?
Дизайн-токен даёт имя дизайн-решению, чтобы каждый потребитель ссылался на имя, а не на сырое значение, — это ход «единый источник истины», применённый к цвету, отступам и типографике. Окупается трёхуровневая модель: примитивы держат сырую палитру, семантические токены несут намерение и являются слоем, который переопределяют темы, а компонентные токены позволяют одному компоненту отклониться. Пиши один раз в формате W3C DTCG JSON (стабилен как v2025.10), затем дай Style Dictionary трансформировать этот один источник в корректные CSS custom properties, iOS Swift и Android XML — сгенерированные, а не скопированные руками, чтобы платформы не разошлись. На web каскад CSS делает темизацию бесплатно: переопредели семантические имена под селектором темы, и каждый компонент перекрасится за одну перерисовку — при условии, что компоненты ссылаются на семантику, а не на примитивы или литералы. Принуждай это lint-правилом, масштабируй уровни под число целей, в которые реально отгружаешь, — и ребрендинг, который занял одиннадцать недель, становится одной правкой.