CSS и дизайн-системы
Дизайн-токены, шкалы, темизация и инструмент стилизации, который подходит вашему стеку.
За 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, длительности и кривые анимации тоже живут в том же файле.
: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-скрипте до первой отрисовки, иначе пользователь увидит вспышку неправильной темы.
: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, чтобы ломающее изменение в кнопке не стало сюрпризом для десяти приложений-потребителей.
// 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)) часто вообще избавляет от брейкпоинтов.
.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 при сборке.
Лучшие практики
- Два слоя токенов: примитивы для палитры, семантические токены для компонентов.
- Компоненты используют только семантические токены, никогда — сырые значения.
- Держите один список брейкпоинтов и одну шкалу отступов в одном файле.
- Для компонентов, которые встречаются в разных раскладках, предпочитайте container queries.
- Стройте сложные виджеты на доступных headless-примитивах.
- Меняйте токены только добавлением и версионируйте общие пакеты.
- Проверяйте контраст цвета для каждой пары текста и фона в каждой теме.
Частые ошибки
- Хардкодить hex и px в компонентах — после этого темизация невозможна.
- Бороться со специфичностью через !important вместо каскадных слоёв или изолированных стилей.
- Выбрать CSS-in-JS с рантаймом для приложения, построенного на серверных компонентах, а потом пометить всё как клиентские компоненты.
- Придумывать брейкпоинты под каждый компонент, из-за чего раскладка прыгает на десятке разных ширин.
- Давать компонентам лазейку для любых стилей — это медленно разрушает единообразие.
Глубже
08Каскадные слои: архитектура для специфичности
Каскадные слои (@layer) решают, какая группа стилей побеждает, ещё до того, как браузер сравнивает специфичность. Вы один раз объявляете порядок, например reset, vendor, tokens, base, components, utilities, и любое правило в более позднем слое побеждает любое правило в более раннем, какой бы специфичный селектор у него ни был. Это заканчивает гонку длинных селекторов и !important. Три детали часто подводят. Стили вне слоёв побеждают все стили в слоях, поэтому чужой CSS, который вы не контролируете, нужно импортировать в нижний слой через @import url(...) layer(vendor), иначе он молча победит. !important переворачивает порядок: важное объявление в раннем слое побеждает важное объявление в позднем, и это позволяет reset-слою защитить правила доступности. И порядок фиксируется в момент, когда имя слоя встречается впервые, поэтому объявляйте полный список в самом начале входного файла стилей, до любого импорта.
@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.
: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 с сервисом визуального сравнения) на каждое изменение токенов, потому что одно значение может сдвинуть сотни компонентов сразу.
{
"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.
@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;
}
Реальные ситуации
White-label клиенту нужен свой бренд через две недели
Проблема
Продукт продают второму клиенту, который хочет свои цвета, шрифт и радиус скругления. В компонентах смешаны токены и захардкоженные значения, и никто не знает, сколько всего сломается.
Как решить
Начните с аудита: найдите в файлах компонентов значения hex, rgb() и px и составьте список; всё, что должно меняться от бренда к бренду, становится семантическим токеном. Затем добавьте маппинг бренда — блок [data-brand], который переназначает семантические токены на новые примитивы, плюс токены font-family и радиусов. Состояния hover, pressed и мягкие оттенки выводите через color-mix() или синтаксис относительных цветов, чтобы клиенту достаточно было дать два-три цвета. Запустите автоматическую проверку контраста по каждой паре текста и фона для обоих брендов в обеих темах. И наконец, включите для папок компонентов правило Stylelint вроде color-no-hex, чтобы новые захардкоженные значения роняли CI. Логика компонентов не меняется — меняются только значения.
Стили сторонних виджетов постоянно перебивают дизайн-систему
Проблема
Date picker и чат-виджет поставляются со своим глобальным CSS. Часть их селекторов побеждает стили ваших компонентов, и команда уже начала отбиваться через !important.
Как решить
Перенесите вендорный CSS в нижний каскадный слой. Объявите порядок в начале входного файла стилей, например @layer reset, vendor, base, components, utilities, и импортируйте CSS виджета с layer(vendor). Теперь любое правило компонента побеждает независимо от специфичности. Удаляйте заплатки с !important по одной, проверяя каждую прогоном визуальной регрессии. Если виджет во время работы вставляет тег style, который нельзя положить в слой, оберните его в контейнер и стилизуйте из слоя components через @scope или изолируйте его в shadow root. Запишите порядок слоёв в contribution guide, чтобы новый код попадал в нужный слой.
Тёмная тема прошла дизайн-ревью, но провалила аудит доступности
Проблема
Команде тёмная тема кажется нормальной, но аудит находит приглушённый текст и текст плейсхолдеров с контрастом ниже 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, которое ограничивает стили поддеревом, при желании с нижней границей, где они перестают действовать.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.