Все уроки
ФронтендСредний12 мин чтенияВ моём стеке

CSS и дизайн-системы

Дизайн-токены, шкалы, темизация и инструмент стилизации, который подходит вашему стеку.

  • Design tokens
  • CSS Modules
  • styled-components
  • Tailwind
  • Responsive
Шаг 1/4
Дизайн-системы нет: каждая кнопка выбрала свой hex и свой радиус. Дизайнер насчитал 47 оттенков «фирменного синего».

За 30 секунд

Дизайн-система — это общий набор решений (цвета, отступы, типографика, радиусы, анимация), сохранённых как именованные токены, которыми пользуется каждый компонент. Вместо того чтобы писать #1a1a1a или 17px в сотне мест, вы пишете var(--color-text) и var(--space-4). В результате интерфейс выглядит единообразно, его легко перекрасить в другую тему, и он остаётся поддерживаемым по мере роста команды и кодовой базы.

Почему я это использую

  • Я использую дизайн-токены в каждом проекте, потому что они превращают сотни мелких визуальных решений в несколько именованных, которые понятны и дизайнеру, и разработчику.
  • Я видел, что бывает без них: файлы переопределений под каждый проект на сотни и тысячи строк с !important, где никто не решается поменять цвет.
  • Мне важен выбор инструмента стилизации, потому что от него зависит, сможет ли компонент рендериться на сервере, сколько JavaScript уйдёт в браузер и насколько болезненным будет редизайн.
  • Понятная адаптивная стратегия (фиксированный список брейкпоинтов плюс container queries) — вот что позволяет вёрстке выдержать реальный контент, реальные языки и реальные устройства.

Как это работает

01Идея: решения, а не значения

Представьте дизайн-систему как свод правил бренда. Сырое значение вроде #0f62fe ничего не говорит; токен --color-accent сообщает, для чего он нужен. Обычно токены делятся на два слоя. Примитивные токены описывают палитру (--blue-600, --gray-100). Семантические токены описывают назначение (--color-accent, --color-surface, --color-text-muted) и ссылаются на примитивы. Компоненты используют только семантические токены. Именно это одно правило делает темизацию и редизайн дешёвыми: вы меняете, на что указывает --color-surface, и за ним следуют все карточки, модальные окна и поля ввода. Токены — это не только цвет: отступы, размеры шрифтов, межстрочные интервалы, радиусы, тени, слои z-index, длительности и кривые анимации тоже живут в том же файле.

css
:root {
  /* primitives */
  --gray-50: #f7f7f8;
  --gray-900: #16161a;
  --blue-600: #2952e3;
 
  /* semantic: components use only these */
  --color-bg: var(--gray-50);
  --color-text: var(--gray-900);
  --color-accent: var(--blue-600);
  --radius-md: 0.5rem;
  --duration-fast: 150ms;
}

02Шкалы отступов и типографики

Шкала — это короткий фиксированный список допустимых размеров. Вместо 13px здесь и 15px там вы выбираете шаги вроде 4, 8, 12, 16, 24, 32, 48, 64 для отступов и модульное соотношение (например, 1.2 или 1.25) для размеров шрифта. Меньше вариантов — быстрее решения и заметный ритм. Fluid-типографика использует clamp(), чтобы заголовки плавно росли между минимумом и максимумом без брейкпоинтов: clamp(2rem, 1.2rem + 3vw, 3.5rem). Задавайте размеры шрифтов в rem, чтобы уважать настройку шрифта в браузере пользователя, а line-height оставляйте без единиц (1.5 для основного текста, от 1.1 до 1.2 для крупных заголовков).

  • Отступы: базовая сетка 4px или 8px, токены от --space-1 до --space-10.
  • Типографика: для большинства продуктов хватает 6–8 шагов.
  • Fluid: clamp(min, preferred, max) для заголовков и отступов секций.
  • Никогда не позволяйте компоненту придумывать размер, которого нет в шкале.

03Темизация: светлая, тёмная и бренды

Поскольку компоненты читают семантические токены, тема — это просто другой набор значений для тех же имён. Тёмная тема может следовать настройке операционной системы через prefers-color-scheme или выбору пользователя, сохранённому в атрибуте data-theme на элементе html. В современном CSS есть ещё функция light-dark(), которая выбирает одно из двух значений в зависимости от свойства color-scheme, так что многие токены можно описать один раз. Для white-label продуктов каждый бренд — это ещё один набор токенов, подключаемый на корневом элементе. Ставьте атрибут темы на сервере или в крошечном inline-скрипте до первой отрисовки, иначе пользователь увидит вспышку неправильной темы.

css
:root {
  color-scheme: light dark;
  --color-bg: light-dark(#ffffff, #121214);
  --color-text: light-dark(#16161a, #ececf1);
}
 
/* explicit user choice wins over the OS setting */
[data-theme="dark"] { color-scheme: dark; }
[data-theme="light"] { color-scheme: light; }

04CSS Modules vs styled-components vs Tailwind

На любом из трёх можно построить хорошую дизайн-систему; они различаются тем, где живут стили и что выполняется в рантайме. CSS Modules — это обычные CSS-файлы с локально изолированными именами классов, компилируемые на этапе сборки, без затрат в рантайме и с полной поддержкой React Server Components. styled-components — это CSS-in-JS: стили живут в компоненте и могут реагировать на пропсы, но генерируются в браузере и зависят от React-контекста, поэтому в Next.js App Router им нужны клиентские компоненты и style registry для SSR. С 2025 года проект находится в режиме поддержки и не лучший выбор по умолчанию для новых RSC-проектов. Tailwind — utility-first: вы собираете классы вроде px-4 text-sm прямо в разметке. Версия 4 задаёт токены в CSS через @theme, генерирует только используемые классы и отлично работает с серверными компонентами.

  • CSS Modules: настоящий CSS, ноль рантайма, безопасно для RSC; имена придумываете сами.
  • styled-components: динамические пропсы и стили рядом с компонентом; цена в рантайме, в RSC — только на клиенте.
  • Tailwind: очень быстрая разработка, крошечный итоговый CSS, безопасно для RSC; длинные списки классов требуют дисциплины.
  • Zero-runtime CSS-in-JS (vanilla-extract, Panda, StyleX) — где-то посередине.

05Библиотеки компонентов

Файл токенов — это фундамент; библиотека компонентов — дом, построенный на нём. Начинайте со скучных, но самых используемых частей: Button, Input, Select, Checkbox, Modal, Toast, Card и примитивов раскладки Stack и Grid. У каждого компонента должен быть небольшой типизированный API (variant, size, disabled), а не приём произвольных стилей — так система остаётся единообразной. Для сложных интерактивных виджетов (диалоги, меню, комбобоксы) опирайтесь на headless-примитивы с готовой доступностью, такие как Radix, React Aria или Ark UI, а не пишите ловушки фокуса и обработку клавиатуры сами. Документируйте каждый компонент живыми примерами и версионируйте общие пакеты по semver, чтобы ломающее изменение в кнопке не стало сюрпризом для десяти приложений-потребителей.

tsx
// Button/index.tsx - a small, closed API
import s from "./style.module.css";
 
type Props = {
  variant?: "primary" | "ghost";
  size?: "sm" | "md";
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
 
export function Button({ variant = "primary", size = "md", className, ...rest }: Props) {
  const cls = [s.button, s[variant], s[size], className].filter(Boolean).join(" ");
  return <button className={cls} {...rest} />;
}

06Адаптивная стратегия и container queries

Выберите один список брейкпоинтов, храните его в одном месте и используйте везде. Mobile-first (min-width) или desktop-first (max-width) — менее важно, чем последовательность. Media queries отвечают на вопрос «какой ширины экран?». Container queries отвечают на более правильный для компонентов вопрос: «какой ширины блок, в котором я нахожусь?». Карточка товара может переключиться с вертикальной раскладки на горизонтальную, когда её контейнер достаточно широк, — неважно, стоит она в сайдбаре или в сетке на всю ширину. Container queries и единица cqi поддерживаются всеми современными браузерами. Используйте и внутренне адаптивные раскладки: grid с repeat(auto-fill, minmax(16rem, 1fr)) часто вообще избавляет от брейкпоинтов.

css
.cardWrap {
  container-type: inline-size;
}
 
.card {
  display: grid;
  gap: var(--space-3);
}
 
@container (min-width: 28rem) {
  .card { grid-template-columns: 10rem 1fr; }
}

07Советы для продакшена

Охраняйте систему инструментами, а не памятью. Stylelint может запретить сырые hex-цвета и значения в px вне файла токенов. Каскадные слои (@layer reset, tokens, components, utilities) делают специфичность предсказуемой, и !important больше не нужен. Изменяйте токены только добавлением: переименование или удаление токена — ломающее изменение, поэтому сначала вводите новый, мигрируете, потом удаляете старый. Тестируйте компоненты на реальных брейкпоинтах и с длинными переведёнными строками, потому что немецкий, русский или армянский текст ломает вёрстку, которая на английском выглядит нормально. И наконец, проверяйте контраст для каждой пары токенов текста и фона в обеих темах.

  • Правило линтера: никаких hex и px вне токенов.
  • Используйте @layer вместо !important.
  • Сначала добавьте токены, мигрируйте, потом удаляйте.
  • Тестируйте с самой длинной локалью и на каждом брейкпоинте.

Когда применять

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

Когда не стоит

  • Одностраничный лендинг, который проживёт несколько недель; хватит нескольких CSS-переменных.
  • Не стройте полноценную библиотеку компонентов, пока UI продукта не устоялся; выносите компоненты после того, как увидели, что они повторяются.

Альтернативы

  • Tailwind CSS

    Utility-классы с токенами в @theme; быстро в разработке, дружит с RSC, навязывает свой стиль разметки.

  • CSS Modules

    Изолированный обычный CSS без рантайма; мой выбор по умолчанию для server-first проектов на Next.js.

  • styled-components / Emotion

    CSS-in-JS в рантайме; удобная стилизация через пропсы, но с React Server Components — только на клиенте.

  • vanilla-extract / Panda CSS / StyleX

    Типобезопасные стили на TypeScript, компилируемые в статический CSS при сборке.

Лучшие практики

  1. Два слоя токенов: примитивы для палитры, семантические токены для компонентов.
  2. Компоненты используют только семантические токены, никогда — сырые значения.
  3. Держите один список брейкпоинтов и одну шкалу отступов в одном файле.
  4. Для компонентов, которые встречаются в разных раскладках, предпочитайте container queries.
  5. Стройте сложные виджеты на доступных headless-примитивах.
  6. Меняйте токены только добавлением и версионируйте общие пакеты.
  7. Проверяйте контраст цвета для каждой пары текста и фона в каждой теме.

Частые ошибки

  • Хардкодить hex и px в компонентах — после этого темизация невозможна.
  • Бороться со специфичностью через !important вместо каскадных слоёв или изолированных стилей.
  • Выбрать CSS-in-JS с рантаймом для приложения, построенного на серверных компонентах, а потом пометить всё как клиентские компоненты.
  • Придумывать брейкпоинты под каждый компонент, из-за чего раскладка прыгает на десятке разных ширин.
  • Давать компонентам лазейку для любых стилей — это медленно разрушает единообразие.

Глубже

08Каскадные слои: архитектура для специфичности

Каскадные слои (@layer) решают, какая группа стилей побеждает, ещё до того, как браузер сравнивает специфичность. Вы один раз объявляете порядок, например reset, vendor, tokens, base, components, utilities, и любое правило в более позднем слое побеждает любое правило в более раннем, какой бы специфичный селектор у него ни был. Это заканчивает гонку длинных селекторов и !important. Три детали часто подводят. Стили вне слоёв побеждают все стили в слоях, поэтому чужой CSS, который вы не контролируете, нужно импортировать в нижний слой через @import url(...) layer(vendor), иначе он молча победит. !important переворачивает порядок: важное объявление в раннем слое побеждает важное объявление в позднем, и это позволяет reset-слою защитить правила доступности. И порядок фиксируется в момент, когда имя слоя встречается впервые, поэтому объявляйте полный список в самом начале входного файла стилей, до любого импорта.

css
@layer reset, vendor, tokens, base, components, utilities;
 
@import url("datepicker.css") layer(vendor);
 
@layer components {
  .button { background: var(--color-accent); }
  .card .button.primary { background: var(--color-accent-strong); }
}
 
@layer utilities {
  /* wins over both rules above, despite lower specificity */
  .bg-surface { background: var(--color-surface); }
}

09Цвет, который можно вычислить: OKLCH, color-mix() и относительные цвета

Hex и HSL плохо подходят для построения шкал. Два цвета HSL с одинаковым значением lightness могут выглядеть совершенно по-разному по яркости, поэтому сгенерированные оттенки непредсказуемо проваливают проверку контраста. OKLCH описывает цвет через воспринимаемую светлоту, хрому (насыщенность) и тон, поэтому шаги светлоты от 0.95 до 0.25 дают ровную на вид шкалу, а о контрасте становится проще рассуждать. Современный CSS умеет и вычислять цвета прямо в браузере. color-mix(in oklch, var(--brand), black 15%) делает hover-оттенок из любого цвета бренда, а синтаксис относительных цветов, oklch(from var(--brand) l c h), позволяет поменять один канал и сохранить остальные. Всё это работает в актуальных версиях основных браузеров. Практическая выгода — white-label: клиент даёт один-два цвета бренда, а система вычисляет состояния hover, pressed, мягкий фон и рамку. Вычисленные цвета — отправная точка, а не гарантия, поэтому всё равно проверяйте каждую пару текста и фона на контраст по WCAG.

css
:root {
  --brand: oklch(0.58 0.17 255);
 
  --color-accent: var(--brand);
  --color-accent-hover: color-mix(in oklch, var(--brand), black 15%);
  --color-accent-subtle: oklch(from var(--brand) 0.95 calc(c * 0.3) h);
  --color-accent-border: oklch(from var(--brand) calc(l + 0.15) c h);
  --color-on-accent: oklch(0.99 0 0);
}
 
[data-brand="b"] {
  --brand: oklch(0.62 0.19 25); /* the only value a new brand changes */
}

10Токены как пайплайн, а не файл

В одном приложении достаточно одного файла tokens.css. Когда есть несколько веб-приложений, нативное мобильное приложение и дизайн-инструмент, токены становятся продуктом со своими потребителями. Источником истины тогда служит JSON-файл в формате W3C Design Tokens Community Group (DTCG), у которого вышла первая стабильная версия 2025.10 и который понимают плагины Figma и инструменты сборки. Трансформер вроде Style Dictionary превращает этот источник в CSS custom properties, типизированный объект TypeScript для автодополнения и константы Swift или Kotlin для мобильных приложений. Относитесь к пакету как к любому публичному API: семантическое версионирование, changelog и депрекации, при которых старое имя остаётся алиасом на одну мажорную версию, а lint-правило предупреждает о его использовании. Запускайте скриншот-тесты (например, Storybook с сервисом визуального сравнения) на каждое изменение токенов, потому что одно значение может сдвинуть сотни компонентов сразу.

json
{
  "color": {
    "blue-600": {
      "$type": "color",
      "$value": { "colorSpace": "srgb", "components": [0.145, 0.388, 0.922], "hex": "#2563eb" }
    },
    "accent": { "$type": "color", "$value": "{color.blue-600}" }
  },
  "space": {
    "4": { "$type": "dimension", "$value": { "value": 1, "unit": "rem" } }
  }
}

11Стоимость CSS во время выполнения на больших масштабах

Производительность CSS редко важна, пока на странице нет тысяч элементов; тогда несколько вещей начинают стоить реального времени. Пересчёт стилей: изменение custom property на :root, например при смене темы, инвалидирует стили каждого элемента, который её наследует. Один раз на клик — нормально; анимировать корневую переменную каждый кадр — нет, поэтому ограничивайте анимируемые переменные элементом, который их использует. Регистрируйте анимируемые переменные через @property с inherits: false: так у них появляется настоящий тип, они плавно интерполируются, а изменение не расходится каскадом по дереву. Неиспользуемый CSS: глобальная таблица стилей только растёт, поэтому предпочитайте CSS на уровне компонентов (CSS Modules или Tailwind, который генерирует только найденные классы) и проверяйте панель Coverage в DevTools. Работа вне экрана: content-visibility: auto вместе с contain-intrinsic-size позволяет браузеру пропускать layout и paint для длинных секций ниже первого экрана. Сопоставление селекторов редко бывает узким местом; прежде чем переписывать селекторы, измерьте их через selector stats в панели Performance.

css
@property --progress {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}
 
.meter {
  background: linear-gradient(90deg, var(--color-accent) var(--progress), transparent 0);
  transition: --progress 300ms ease-out;
}
 
.long-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px;
}

Реальные ситуации

01

White-label клиенту нужен свой бренд через две недели

Проблема

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

Как решить

Начните с аудита: найдите в файлах компонентов значения hex, rgb() и px и составьте список; всё, что должно меняться от бренда к бренду, становится семантическим токеном. Затем добавьте маппинг бренда — блок [data-brand], который переназначает семантические токены на новые примитивы, плюс токены font-family и радиусов. Состояния hover, pressed и мягкие оттенки выводите через color-mix() или синтаксис относительных цветов, чтобы клиенту достаточно было дать два-три цвета. Запустите автоматическую проверку контраста по каждой паре текста и фона для обоих брендов в обеих темах. И наконец, включите для папок компонентов правило Stylelint вроде color-no-hex, чтобы новые захардкоженные значения роняли CI. Логика компонентов не меняется — меняются только значения.

02

Стили сторонних виджетов постоянно перебивают дизайн-систему

Проблема

Date picker и чат-виджет поставляются со своим глобальным CSS. Часть их селекторов побеждает стили ваших компонентов, и команда уже начала отбиваться через !important.

Как решить

Перенесите вендорный CSS в нижний каскадный слой. Объявите порядок в начале входного файла стилей, например @layer reset, vendor, base, components, utilities, и импортируйте CSS виджета с layer(vendor). Теперь любое правило компонента побеждает независимо от специфичности. Удаляйте заплатки с !important по одной, проверяя каждую прогоном визуальной регрессии. Если виджет во время работы вставляет тег style, который нельзя положить в слой, оберните его в контейнер и стилизуйте из слоя components через @scope или изолируйте его в shadow root. Запишите порядок слоёв в contribution guide, чтобы новый код попадал в нужный слой.

03

Тёмная тема прошла дизайн-ревью, но провалила аудит доступности

Проблема

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

Как решить

Сделайте контраст тестом, а не вопросом вкуса. Экспортируйте для каждой темы все семантические пары текста, иконок и рамок и проверяйте их в CI библиотекой контраста WCAG: падение ниже 4.5:1 для текста и 3:1 для элементов интерфейса. Исправляйте на уровне токенов: поднимите светлоту --color-text-muted в тёмном маппинге, вместо того чтобы латать компоненты. Дайте фокусу свою пару токенов, например --focus-ring и --focus-ring-offset, определённую для каждой темы, и рисуйте двухцветное кольцо (outline плюс контрастный box-shadow), чтобы оно было видно на любом фоне. Затем проверьте ещё раз в режиме Windows forced-colors, где пользовательские цвета заменяются системными.

Шпаргалка

Объявить порядок слоёв один раз
@layer reset, vendor, tokens, base, components, utilities;
Импортировать вендорный CSS в нижний слой
@import url("vendor.css") layer(vendor);
Hover-оттенок из любого цвета
color-mix(in oklch, var(--color-accent), black 15%)
Изменить один канал цвета
oklch(from var(--brand) calc(l + 0.2) c h)
Значение под тему без media queries
color-scheme: light dark; color: light-dark(#111, #eee);
Fluid-типографика, уважающая зум
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
Адаптивное правило на уровне компонента
.wrap { container-type: inline-size; } @container (width > 40rem) { ... }
Типизированное анимируемое custom property
@property --x { syntax: "<length>"; inherits: false; initial-value: 0px; }
Запретить hex-цвета (Stylelint)
"color-no-hex": true
Не рендерить секции вне экрана
content-visibility: auto; contain-intrinsic-size: auto 600px;

В моих реальных проектах

  • На e-commerce платформе зоотоваров я записал правила дизайн-системы в AI-инструкции проекта: только дизайн-токены, 7 фиксированных брейкпоинтов, никаких hex и px в компонентах и изменения темы только добавлением.
  • Я совместно поддерживаю внутреннюю экосистему пакетов с базовым набором токенов отступов и дизайн-системой админки примерно из 20 компонентов, а также дизайн-инспектор, который позволяет дизайнерам настраивать CSS-переменные вживую и сохранять их обратно в исходники.
  • В этой экосистеме я спланировал переход на семантические токены, чтобы убрать файлы переопределений под каждый проект размером примерно от 383 до 2,248 строк каждый.
  • Я выпускал проекты на всех трёх подходах: styled-components на нескольких витринах на Next.js, Tailwind в админке SaaS и CSS Modules с единым файлом токенов на этом сайте.

Вопросы и ответы

Q1Что такое дизайн-токен и зачем два слоя?

Дизайн-токен — это именованное дизайн-решение, например цвет, шаг отступа или длительность, которое хранится в одном месте и используется везде, обычно как CSS custom property. Два слоя сохраняют гибкость. Примитивные токены описывают, что есть, например --blue-600. Семантические токены описывают назначение, например --color-accent или --color-surface, и ссылаются на примитивы. Компоненты потребляют только семантические токены. Тогда тёмная тема, ребрендинг или white-label клиент — это просто новое сопоставление семантических имён с примитивами, и код компонентов не меняется. Без семантического слоя вы в итоге ищете каждое место, где использовался blue-600, и гадаете, какие из них на самом деле были «акцентом».

Q2CSS Modules, styled-components или Tailwind для нового проекта на Next.js App Router?

Для приложения, построенного на серверных компонентах, я бы выбрал CSS Modules или Tailwind. Оба компилируются в статический CSS при сборке, не добавляют JavaScript в рантайме и работают в серверных компонентах. CSS Modules подходят командам, которые любят писать настоящий CSS с токенами и хотят чистую разметку; Tailwind — командам, которые ценят скорость и спокойно относятся к utility-классам. styled-components зависит от React-контекста и вставки стилей в рантайме, поэтому в App Router заставляет использовать клиентские компоненты и SSR registry, а сам проект с 2025 года в режиме поддержки. В существующих кодовых базах, где он работает, я бы его оставил, но новый проект на нём начинать не стал бы.

Q3Когда вы используете container queries вместо media queries?

Media queries — про вьюпорт; container queries — про собственное пространство компонента. Media queries я использую для раскладки на уровне страницы, например чтобы переключить навигацию или число колонок сетки. Container queries — для переиспользуемых компонентов, которые появляются в разных контекстах, например для карточки товара, которая на одной странице стоит в узком сайдбаре, а на другой — в широкой сетке. С container-type: inline-size на обёртке и правилом @container карточка подстраивается под свою реальную ширину. Это убирает целый класс багов, когда компонент выглядит хорошо на большом экране, но сжат в узкой колонке.

Q4Как реализовать тёмную тему без вспышки неправильной темы?

Во-первых, все компоненты должны читать семантические токены цвета, тогда тема — это лишь другой набор значений. Во-вторых, тему нужно определить до первой отрисовки. Если у пользователя есть сохранённое предпочтение, прочитайте его из cookie на сервере и отрендерьте data-theme на элементе html, или запустите крошечный inline-скрипт в head, который выставит его из localStorage. Если предпочтения нет, следуйте prefers-color-scheme через color-scheme и light-dark(). Не переключайте тему в React-эффекте после гидрации: к этому моменту страница уже отрисована в теме по умолчанию, и пользователь видит вспышку.

Q5Как сохранить единообразие дизайн-системы в команде?

Инструментами и маленькой поверхностью API, а не напоминаниями. Токены живут в одном файле, а правило Stylelint запрещает сырые цвета и значения в пикселях везде, кроме него. Компоненты предоставляют закрытый набор вариантов и размеров вместо произвольных style-пропсов. Общие компоненты задокументированы живыми примерами и версионируются, так что потребители обновляются осознанно. На код-ревью мы проверяем, не появились ли новые разовые значения, а правила я записываю в contribution guide проекта или в AI-инструкции, чтобы им следовали и ассистенты. Когда кому-то нужно то, чего в системе нет, мы правильно добавляем это в систему, а не латаем локально.

Q6Как сделать дизайн-систему доступной?

Доступность начинается с токенов: каждая пара текста и фона должна соответствовать контрасту WCAG — 4.5:1 для обычного текста и 3:1 для крупного текста и элементов интерфейса — в каждой теме. Стили фокуса — тоже токен, и их никогда не удаляют, только перестилизуют через :focus-visible. Интерактивные компоненты используют настоящие элементы: button для действий и a для навигации, а сложные виджеты строятся на headless-примитивах, которые уже умеют работать с клавиатурой, удерживать фокус и выставлять ARIA. Размеры задаются в rem, чтобы уважать настройки шрифта пользователя, области нажатия — не меньше 24 на 24 CSS-пикселя, а токены анимации учитывают prefers-reduced-motion.

Вопросы уровня senior

S1Как бы вы выкатили ломающее изменение токенов на десяток приложений-потребителей?

Я отношусь к пакету токенов как к публичному API. Во-первых, я никогда не переименовываю на месте: добавляю новый семантический токен, оставляю старый как алиас, указывающий на него, и помечаю его устаревшим в сгенерированных типах TypeScript и в lint-правиле, которое предупреждает об использовании. Это выходит минорной версией, так что приложения обновляются без поломок. Во-вторых, я даю codemod, часто простой скрипт на PostCSS или find-and-replace, который переписывает старые имена. В-третьих, я отслеживаю внедрение: CI-задача, которая ищет устаревшие имена в каждом репозитории, даёт burn-down список. Только когда использование доходит до нуля или после объявленной даты, я удаляю алиас в мажорной версии. Тесты визуальной регрессии в каждом приложении ловят случаи, где алиас на самом деле не был эквивалентен. Компромисс — более медленный rollout в обмен на отсутствие неожиданных поломок.

S2Темизация на этапе сборки или во время выполнения для мультибрендового продукта?

Мой выбор по умолчанию — темизация во время выполнения через CSS custom properties: один CSS-бандл, бренд или тема — это data-атрибут, который переназначает семантические токены, а переключение стоит одного пересчёта стилей. Это подходит и для тёмной темы, которую включает пользователь, и для брендинга тенанта, загруженного из базы, без пересборки под каждого тенанта. Темизация на этапе сборки генерирует таблицу стилей на каждый бренд; каждый бренд отдаёт чуть меньше CSS, и вы получаете проверки при компиляции, но каждый новый бренд требует сборки и деплоя, а переключение означает перезагрузку. Многие команды комбинируют подходы: палитры генерируются при сборке из источника токенов, а семантический маппинг применяется во время выполнения. Чего я избегаю, так это темизации через JavaScript theme provider в React: он вынуждает делать клиентские компоненты и перерендеривает дерево при каждой смене темы, что вредит и серверному рендерингу, и отзывчивости интерфейса.

S3Как Tailwind сочетается с системой токенов?

Они дополняют друг друга: токены решают, какие значения существуют, а Tailwind — удобный способ их применять. Tailwind v4 настраивается в CSS: вы объявляете токены внутри блока @theme, и он генерирует утилиты вроде bg-accent или p-4, а также открывает каждое значение как CSS-переменную, которую может читать обычный CSS. Я переношу семантические токены в @theme и сбрасываю стандартную палитру через --color-*: initial, чтобы никто не мог потянуться за произвольным blue-500. Произвольные значения вроде p-[13px] разрешены, но помечаются на ревью. Сложные компоненты всё ещё могут использовать CSS Modules, читающие те же переменные. Реальный риск утилит — дрейф через разовые значения и длинные нечитаемые списки классов; лечится это небольшим набором общих компонентов-примитивов и lint-правилами, а не отказом от Tailwind.

Глоссарий

Каскадный слой
Именованная группа стилей (@layer). Более поздние слои побеждают ранние ещё до сравнения специфичности.
Специфичность
Оценка браузером того, насколько точен селектор; она решает конфликты внутри одного слоя и источника.
OKLCH
Цветовое пространство с воспринимаемой светлотой, хромой и тоном, где равные шаги выглядят для глаза одинаково различными.
color-mix()
CSS-функция, которая смешивает два цвета в выбранном цветовом пространстве, например для hover-оттенков.
Синтаксис относительных цветов
Построение нового цвета из существующего с изменением выбранных каналов: oklch(from var(--x) l c h).
Формат DTCG
JSON-стандарт дизайн-токенов от community group W3C, который читают дизайн-инструменты и инструменты сборки.
Style Dictionary
Инструмент сборки, который превращает один источник токенов в CSS, JavaScript, iOS и Android.
@property
Регистрирует custom property с типом, начальным значением и наследованием, чтобы её можно было анимировать и валидировать.
Пересчёт стилей
Этап, на котором браузер вычисляет итоговые стили каждого элемента; большие инвалидации делают его медленным.
@scope
At-правило CSS, которое ограничивает стили поддеревом, при желании с нижней границей, где они перестают действовать.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.