22 янв. 2025 г.·7 мин чтения

Дизайн-токены React для переключения тем в больших приложениях

Дизайн-токены React помогают большим командам держать цвета, отступы и темы в рантайме согласованными. Узнайте, когда CSS-переменные работают лучше, чем самодельные решения на context.

Дизайн-токены React для переключения тем в больших приложениях

Почему код тем становится беспорядочным в больших React-приложениях

Код тем редко ломается разом. Он расползается незаметно. Одна команда добавляет кнопку с жёстко прописанным синим цветом, другая копирует её в модалку, а третья подправляет цвет при наведении, потому что «и так достаточно похоже». Через несколько месяцев приложение всё ещё кажется в целом единым в светлой теме, поэтому никто не спешит всё переделывать.

Потом приходит тёмная тема, и все обходные пути всплывают сразу. Текст, который нормально смотрелся на белом фоне, на тёмно-сером становится грязным. Границы исчезают. Одна карточка использует чуть более тёплый фон, чем остальные, и теперь она сразу бросается в глаза. Маленькие различия, которые раньше никто не замечал, становятся очевидными, потому что правила контраста уже другие.

В больших React-приложениях это только усугубляется, потому что компоненты живут долго и переходят между продуктами. Строка поиска появляется в основном приложении, потом попадает в админку, а затем всплывает во внутреннем инструменте. Если каждая команда копировала код тем вместо того, чтобы делиться токенами, версии быстро расходятся. В одном приложении обновляют отступы и цвета. В других остаются старые значения, потому что за уборку никто не отвечает.

На самом деле проблема не в стилях. Это проблема координации.

Дизайн, продукт и разработка часто называют одну и ту же вещь по-разному. Один говорит «брендовый синий», другой — «основной», а третий вставляет hex-значение прямо в компонент. Без общей системы каждая команда принимает локальные решения, которые кажутся безобидными. Вместе они создают интерфейс, который дорого менять.

Вот тут-то и начинают играть роль дизайн-токены React. Они дают командам одно место, где можно задать цвета, отступы, радиусы и типографику до того, как эти решения разойдутся по десяткам компонентов и нескольким приложениям.

Обычно боль проявляется знакомо. Кнопка из одного приложения немного не совпадает с кнопкой в другом. Тёмная тема исправляет заголовки, но пропускает поля форм. Новый продукт выходит с копией файла темы и начинает свою собственную версию истины. К тому моменту, когда пользователи замечают проблему, самое сложное — уже не выбрать цвета. Самое сложное — добиться, чтобы несколько команд договорились об одном наборе правил и придерживались его.

Что должно быть в системе токенов

Систему токенов лучше начинать с меньшего набора, чем ожидают многие команды. В больших приложениях первый слой обычно включает цвета, отступы, типографику, радиусы и тени. Эти пять групп закрывают большую часть визуальных решений и не дают людям подсовывать в компоненты случайные hex-коды, значения в пикселях и одноразовые стили.

Цвета обычно привлекают больше всего внимания, но отступы и типографика не менее часто создают беспорядок. Аккуратный набор типографики включает семейство шрифтов, размер, насыщенность и межстрочный интервал. Отступы лучше строить по шкале, которую легко запомнить, например 4, 8, 12, 16 и 24, а не по бесконечным кастомным промежуткам.

Разделение на сырьевые и семантические токены очень важно. Сырьевые токены хранят реальные значения, например blue-600, gray-950, 8px или shadow-2. Семантические токены описывают смысл, например surface, text-muted, border-default или danger.

Такое разделение экономит время позже. Если маркетинг меняет фирменный цвет, команда один раз обновляет сырьевое значение и оставляет семантические имена без изменений. Дизайн-токены React тоже остаются понятнее, потому что компонент с surface и text-primary говорит о себе больше, чем компонент с #0f172a и #e5e7eb.

На уровне компонентов нужна большая сдержанность. Добавляйте такие токены только тогда, когда один и тот же шаблон компонента повторяется достаточно часто, чтобы заслужить собственные имена. Кнопке, которая используется в шести продуктах, могут понадобиться токены для отступов, фона, текста и состояния при наведении. А вот промо-карточке на одной странице — скорее всего, нет.

Хорошее правило простое: если необходимость в повторном использовании ещё не очевидна, остановитесь на семантических токенах. Команды создают много шума, когда слишком рано придумывают имена вроде checkout-sidebar-banner-title-color для того, что может исчезнуть уже в следующем спринте.

Правила именования нужно определить до того, как список токенов начнёт расти. Выберите один шаблон и придерживайтесь его. Многие команды хорошо работают с названиями вроде:

  • color-surface-default
  • color-text-muted
  • space-4
  • radius-sm
  • shadow-card

Сам шаблон менее важен, чем единообразие. Заранее решите, как вы называете состояния, размеры и алиасы. Если одна команда пишет bg-primary, а другая — surface-brand, набор токенов превращается в задачу по переводу.

Хорошая система токенов выглядит немного скучной. Это хороший знак. Когда люди могут угадать правильное имя ещё до поиска, система достаточно ясна, чтобы масштабироваться.

Где хранить токены, чтобы команды могли делиться ими

Если каждое приложение хранит свои файлы цветов, отступов и типографики, расхождения начинаются очень быстро. Одна команда вручную меняет синий оттенок, другая переименовывает токен отступа, и вскоре весь набор продуктов выглядит несогласованным. Для дизайн-токенов React вопрос хранения — это прежде всего вопрос команды, а уже потом вопрос кода.

Храните исходные токены в одном общем месте. В небольшой установке это может быть папка внутри основного репозитория. В более крупной — общий пакет, который импортирует каждое приложение. Смысл простой: люди редактируют один источник, а не пять копий.

Хорошо работают несколько моделей хранения:

  • одна папка в monorepo для всех токенов
  • один внутренний пакет, опубликованный для web- и mobile-приложений
  • один репозиторий design-tokens, если команды работают в разных кодовых базах

Формат важен меньше, чем обычно думают. Выбирайте то, что команда сможет прочитать в code review, не останавливаясь на расшифровке. JSON подходит большинству команд. TypeScript тоже нормален, если разработчики сами владеют слоем токенов и хотят проверку типов. Сложный формат редко оправдан, если его понимает только один человек.

Общий источник должен порождать сгенерированные файлы для каждого приложения. Веб-приложению могут понадобиться CSS-переменные. Storybook может потребовать объект темы. Другому приложению — типизированная карта имён токенов. Считайте всё это выходными файлами. Не позволяйте командам редактировать их вручную, иначе они снова разъедутся.

Версионируйте изменения токенов так же, как обычный продуктовый код. Храните их в Git. Проверяйте их. Помечайте релизы тегами. Если переименование токена ломает экраны в трёх приложениях, к такому изменению нужно относиться с той же осторожностью, что и к любой другой общей зависимости. Здесь помогает семантическое версионирование: небольшие правки цвета могут уйти в minor-релиз, а удалённые или переименованные токены должны запускать major-релиз.

Простое правило работает отлично: один источник, много выходов, никаких ручных копий. Команды двигаются быстрее, когда точно знают, где живут токены и как изменения попадают во все приложения.

Когда CSS-переменные лучше самодельных обходов на context

CSS-переменные выигрывают там, где браузер уже умеет делать работу сам. Значения цветов, отступов, радиусов, теней и типографики естественно наследуются по DOM. Если загонять каждый токен через React context, каждое переключение темы может превратиться в работу со state приложения, которая React вообще не нужна.

Разница особенно заметна в больших приложениях. Переключение темы должно ощущаться как смена краски, а не как событие компонента. Когда вы задаёте один класс или data-атрибут на корневом элементе, браузер обновляет всех потомков, которые используют эти переменные. Не нужно протаскивать объекты токенов через providers, перечитывать их в hooks и подключать theme props к компонентам, которым нужен только цвет фона или текста.

Где CSS-переменные подходят лучше всего

Используйте CSS-переменные для любых значений, которые должны стекать вниз по дереву:

  • семантические цвета вроде surface, text, border и danger
  • шкалы отступов и размеров
  • размеры шрифта и межстрочные интервалы
  • тени, радиусы и длительности анимаций

Компоненты могут читать эти значения в CSS modules, обычном CSS или в styled-системах, которые выводят настоящий CSS. Так React-компоненты остаются проще. Кнопка должна запрашивать класс, а не целую карту токенов.

Часто лучше всего работает простая схема. Храните значения темы под :root или под селектором корня вроде [data-theme="dark"]. Затем переключайте темы, меняя этот один атрибут на html или body. Для переключения тем в React этого обычно достаточно для визуального слоя.

React context лучше оставить для пользовательских предпочтений и логики приложения. Context — хорошее место для таких вещей, как theme = dark, theme = system или theme = high-contrast. Это плохое место для сотен живых значений токенов. Дизайн-токены React по-прежнему важны, но React не обязан тащить через runtime каждый цвет и каждый отступ.

Небольшой пример: у продуктового набора есть маркетинговые страницы, админ-панель и внутренний инструмент. Если все три читают --color-text и --color-surface из корня, они могут использовать одни и те же имена токенов, даже если сами файлы темы у них разные. Это лучше масштабируется, чем самодельные обходы через context, и гораздо проще отлаживается в браузере.

Как добавить переключение темы в рантайме

Объедините продуктовую линейку
Настройте один общий источник токенов для дашбордов, админок и внутренних инструментов.

Переключение темы в рантайме ломается, когда приложение на долю секунды показывает неправильные цвета. Обычно это происходит потому, что браузер успевает отрисовать страницу до того, как узнает, какую тему использовать. Задайте начальную тему на корневом элементе до первого рендера — с помощью маленького скрипта, который читает сохранённый выбор. Если сохранённого выбора нет, используйте системную настройку.

Хорошо работает простой порядок. Сначала проверяйте настройки аккаунта у вошедших пользователей, потому что этот выбор должен сохраняться между устройствами. Если настроек аккаунта нет, читайте local storage. Если нет и его, используйте prefers-color-scheme.

Пользователь, который выбрал тёмную тему на прошлой неделе, не должен открывать приложение и сначала видеть светлую тему, даже на 200 миллисекунд. Люди это замечают.

Для самого переключателя обновляйте одну вещь на верхнем уровне — обычно атрибут data-theme на html или body. Дальше пусть всю тяжёлую работу сделают CSS-переменные. React может хранить текущее имя темы, но ему не нужно протаскивать значения цветов через props или custom context во все компоненты. В больших приложениях такой небольшой выбор интерфейса легко превращается в проблему рендеринга.

Вот где CSS-переменные для темизации обычно выигрывают. Когда корневая тема меняется, кнопки, формы, карточки и поверхности макета обновляются без переписывания каждого компонента по отдельности. Это делает дизайн-токены React практичными, а не хрупкими.

Одну деталь часто упускают: переключение темы не заканчивается на основных компонентах. Проверьте всё, что рендерится вне обычного дерева или использует свои стили.

  • Portals, такие как модалки, подсказки и выпадающие списки
  • Графики, которые кэшируют цвета при первом рендере
  • Редакторы, виджеты выбора даты и другие сторонние виджеты
  • Превью писем, встраивания или iframe, которым нужна отдельная тема

Если графики или виджеты сами не реагируют на CSS-переменные, переинициализируйте их или передавайте новые значения токенов при смене темы. Проверяйте переключение там, где люди пользуются им каждый день, а не только на чистой демо-странице. Именно там начинается дрейф темы.

Реальный пример для набора продуктов

Представьте одну компанию с тремя React-приложениями: клиентской панелью, админкой и приложением настроек. Над каждым работают разные команды, но пользователи всё равно воспринимают их как один продукт. Если цвета, отступы или состояния кнопок расходятся, весь набор начинает ощущаться как что-то собранное наспех.

Общий набор токенов это исправляет. Начните с семантических имён, которые описывают смысл, а не сырые значения: surface-default, text-primary, border-muted, action-primary, danger-bg. Все три приложения читают один и тот же пакет токенов, поэтому говорят на одном визуальном языке, даже если их экраны очень разные.

Панель клиента может использовать surface-default для карточек и action-primary для фильтров графиков. Админка применяет те же токены в таблицах, уведомлениях и инструментах модерации. Приложение настроек использует их для форм, переключателей и подсказок. Никому не нужно гадать, должен ли синий button использовать #2F6BFF или #3A70F2. Токен уже даёт ответ.

При этом у каждого приложения может оставаться небольшой локальный слой. Это важно, потому что не у каждого экрана одинаковые потребности. Панели клиента могут понадобиться цвета графиков, админке — более сильные предупреждения, а приложению настроек — мягкий фон для сгруппированных секций формы.

Но локальный слой должен быть тонким. Добавляйте токены вроде chart-series-a или settings-panel-bg, но по возможности связывайте их с общей семантической прослойкой. Если каждая команда придумывает свою систему названий, дрейф начинается снова уже через месяц.

Вот где дизайн-токены React реально окупаются. Измените один фирменный цвет в общем источнике токенов, опубликуйте обновление, и все приложения получат новое значение action-primary. Кнопки, ссылки, активные вкладки и состояния фокуса двигаются вместе, а не ломаются по одному экрану за раз.

CSS-переменные делают это ещё чище на практике. Поместите общие семантические токены в корневую тему, дайте каждому приложению добавить несколько локальных алиасов — и набор продуктов останется согласованным, при этом у каждого продукта будет место для своих исключений. Обычно этого достаточно для большой семьи приложений, не превращая темизацию в отдельный побочный проект.

Ошибки, которые вызывают дрейф темы

Закройте пробелы тёмной темы
Найдите места, где тёмная тема ломается в формах, модалках, графиках и старом интерфейсе.

Дрейф темы начинается с маленьких shortcuts. Одна команда выпускает быстрое переопределение, другая жёстко прописывает цвет в модалке, и через несколько месяцев один и тот же фирменный цвет появляется в четырёх разных оттенках.

Частая ошибка — называть токены по тому, как они выглядят сегодня, а не по тому, что они значат. Имена вроде blue500 или grayText2 кажутся удобными, пока палитра свежая. Со временем они стареют плохо. Когда дизайн меняется, имя токена перестаёт совпадать со своей ролью. Семантические имена вроде color-action-primary или text-muted держатся намного лучше, потому что смысл остаётся стабильным, даже если hex-значение меняется.

Ещё один источник дрейфа — передавать значения темы через custom context React в каждом компоненте. Сначала это выглядит аккуратно, но потом логика темы расползается по всему дереву. Кнопки, карточки, формы и диалоги начинают тянуть значения цветов и отступов через hooks или props. В итоге кода становится больше, чем нужно браузеру. В больших приложениях CSS-переменные обычно делают эту работу с меньшим трением и меньшим количеством сюрпризов.

Одноразовые переопределения тоже создают проблемы. Продуктовая команда чинит специальную страницу к запуску, а потом никто не убирает переопределение после того, как дизайн-система ушла дальше. Старое значение остаётся в углу приложения. Позже кто-то копирует этот компонент, и несоответствие начинает расползаться.

Смешивание сырых значений и семантических токенов в одном слое только ухудшает ситуацию. Если один компонент использует #1a73e8 напрямую, а другой — color-action-primary, у вас уже нет настоящей системы токенов. У вас полутонизированная система токенов, которой сложнее доверять.

Эти тревожные сигналы появляются заранее:

  • имена токенов описывают hex-цвет вместо роли
  • компоненты читают данные темы из custom context для базовой стилизации
  • старые переопределения остаются после редизайна
  • команды по-прежнему вставляют сырые значения цветов, отступов или радиусов прямо в компоненты
  • embeds и старые части интерфейса не имеют значений по умолчанию

Fallback-значения важнее, чем многие думают. Если вы выпускаете встраиваемые виджеты, устаревшие страницы или старые административные экраны, некоторые части могут загрузиться до того, как активная тема станет доступна. Без fallback-значений в CSS-переменных текст может пропасть, а фоны — сломаться. Система дизайн-токенов React выглядит намного чище, когда у каждого семантического токена есть значение по умолчанию, а старые поверхности всё ещё могут безопасно отрисоваться.

Быстрые проверки перед запуском

Проверьте общие UI-правила
Разберите, что должно жить в общих токенах, а что лучше оставить внутри каждого приложения.

Проблемы с запуском обычно всплывают в скучных местах: новая кнопка, общий заголовок, переключатель тёмной темы, который на секунду показывает белый экран. Система дизайн-токенов React готова, когда команда может добавлять, переключать и отслеживать стили без лишнего связующего кода.

Перед запуском проведите пять небольших проверок.

  • Соберите один новый компонент с нуля, используя только семантические токены вроде color.text.primary или surface.default. Если компоненту нужны кастомные props для темы, дополнительные providers или ручное сопоставление в нескольких файлах, в системе всё ещё есть пробелы.
  • Переключите тёмную тему, пока приложение остаётся смонтированным. Цвета, отступы и состояния должны обновляться на месте. Если нужен полный remount, пользователи заметят вспышки, потерю состояния формы или странные переходы.
  • Откройте два приложения, которые используют один и тот же визуальный язык. Они должны применять одни и те же семантические имена, даже если каждое приложение сопоставляет их с разными фирменными значениями. Если одно приложение называет токен primaryBlue, а другое — ctaMain, дрейф уже начался.
  • Попросите одного дизайнера и одного разработчика проследить токен от источника до экрана. Они должны быстро найти исходный файл, семантическое имя и итоговую CSS-переменную. Если этот путь напоминает детективное расследование, поддержка будет тормозить.
  • Уберите несколько старых theme props после миграции. Если ничего не ломается — отлично. Если половина библиотеки компонентов всё ещё зависит от fallback props, переход выполнен лишь наполовину.

Быстрая проверка тоже помогает. Войдите в систему, откройте настройки, переключите тему, перейдите на другую страницу и откройте модалку. Если заголовок сохраняет старые цвета или модалка игнорирует токены, исправьте это сразу. Команды копируют существующие шаблоны, поэтому один неаккуратный компонент часто превращается в десять.

CSS-переменные для темизации обычно делают эти проверки проще. Они позволяют менять runtime-темы без пересборки всего дерева React и держат стили ближе к браузеру, а не прячут их в самодельном коде на context.

Одно правило сильно экономит силы: каждый новый компонент должен потреблять токены так же, как и каждый старый. Большая часть дрейфа темы начинается с одного послабления, которое потом никто не убирает.

Следующие шаги для стабильного запуска

Начните с одной общей области и сделайте её скучной. Кнопки, поверхности страниц и цвета текста — хороший первый срез, потому что их используют все приложения, а поломки люди замечают быстро. Если этот срез работает и в светлой, и в тёмной теме, остальной запуск пойдёт намного проще.

Слишком раннее переименование файлов обычно только тратит время. Сначала запишите слои токенов на одной странице, чтобы все команды использовали одну и ту же модель. Для большинства больших React-приложений достаточно трёх слоёв:

  • сырые значения вроде цветовых шкал, отступов и радиусов
  • семантические токены вроде surface, text-muted и border-default
  • component tokens для случаев вроде primary-button-bg или input-focus-ring

Такая карта потом снимает споры. Она же делает хранение токенов менее хаотичным, потому что люди понимают, что должно жить в общих пакетах, а что лучше оставить рядом с компонентом.

Скриншоты используйте с первого дня. Небольшой визуальный набор тестов ловит ошибки, которые не видят unit-тесты, например disabled button с неправильным цветом поверхности или заголовок модалки, который берёт старое имя токена. Даже простая галерея before-and-after для десяти типовых экранов может сэкономить часы ручной проверки.

Внедряйте по замене, а не по переписыванию. Сначала переносите общие примитивы. Потом формы, навигацию и состояния статуса. Оставьте одноразовые маркетинговые страницы или старые административные экраны на конец, если только они не используют ту же самую библиотеку компонентов.

Если работа всё время буксует, проблема часто не в усилиях. Проблема в архитектурном дрейфе: токены живут в трёх местах, runtime-темы латками закрывают старые решения, а каждое приложение понемногу нарушает правила. В этот момент помогает внешний аудит.

Oleg Sotnikov может провести ревизию архитектуры токенов и переключения тем как фракционный CTO. Его опыт охватывает крупные production-системы, общие платформы и практическую архитектуру, поэтому разговор может оставаться сосредоточенным на том, что ваша команда реально сможет выпустить на следующей неделе, а не на теории.

Когда дрейф темы начинает тормозить релизы между приложениями, запишитесь на консультацию. Короткий разбор может дать вам порядок внедрения, более чистую карту токенов и чёткую границу между тем, что должно жить в CSS-переменных, и тем, что лучше оставить в коде React.