UX и проектирование интерфейсов Дизайн-системы: компоненты, токены, документация, жизнь системы
0%

Дизайн-системы: компоненты, токены, документация, жизнь системы

Дизайн-системы: компоненты, токены, документация, жизнь системы

Типичная сцена. В продукте четыре команды. Биллинг рисует кнопку «Оплатить» с радиусом 8 и высотой 44. Онбординг — радиус 6, высота 40. Админка берёт кнопку из макета 2022 года: радиус 4, высота 36. Дизайнер видит это на демо и говорит «надо унифицировать». Через месяц появляется файл «UI Kit v1» с одной правильной кнопкой. Ещё через полгода в продакшене по-прежнему три кнопки, а в макетах уже пять, потому что каждый, кому не хватило варианта, скопировал компонент и отвязал.

Это не история про лень. Это история про то, что UI-кит — не дизайн-система. Система начинается не с файла, а с ответа на вопрос: если завтра поменять радиус кнопки, сколько дней пройдёт до того, как он поменяется во всех продуктах, и кто имеет право это решить? Если ответ «никогда» или «непонятно кто» — системы нет, есть картинки. Дальше — про инженерную часть дизайна: из чего система состоит, как устроены токены и API компонентов, что должно быть в документации, как учитывать доступность в макете, как выглядит нормальная передача в разработку (и почему «пиксель в пиксель» — вредная цель) и как система живёт: версии, депрекейшн, миграции, метрики принятия.

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

Формально: дизайн-система — это набор общих решений об интерфейсе плюс механизм их распространения плюс договорённость о том, кто эти решения меняет. Убрать нельзя ни одну из трёх частей. Решения — токены, компоненты, паттерны, правила текста и поведения; это то, что видно. Механизм распространения — пакет в npm, библиотека в макетах, генератор темы, линтер, кодмоды, релизные заметки; без него решения остаются в голове автора. Договорённость — кто владеет, как предложить изменение, что считается ломающим, за сколько недель убирается старый компонент; без неё система разъезжается на второй квартал.

Часто путают с системой Почему это не система
Файл с компонентами в Figma Нет связи с кодом: макет обновили, продукт нет
npm-пакет с компонентами Нет правил применения: компоненты есть, интерфейсы всё равно разные
Брендбук в PDF Описывает логотип и цвета, не описывает состояния и поведение
Скриншоты «как надо» Не проверить, не обновить, не масштабировать
Tailwind или Material из коробки Это чужие решения чужих задач; система — ваши решения, возможно, поверх чужой базы

Полезная проверка: система — это продукт, у которого есть пользователи, и пользователи — продуктовые команды. У продукта есть версии, поддержка, документация, каналы обратной связи и метрика успеха. Если ни одно из этих слов к вашей «системе» не применимо, это библиотека картинок.

Зачем: экономика, а не насмотренность

Стоимость дублирования. Каждый повтор одного решения стоит трижды: придумать, реализовать, потом поддерживать расхождение. Если четыре команды независимо делают модалку, вы платите за четыре модалки и получаете четыре разных поведения фокуса, четыре реакции на Escape и четыре бага доступности. Когда придёт требование «закрывать по клику вне области», вы заплатите четыре раза снова.

Стоимость несогласованности для пользователя. Она не в том, что «некрасиво». Человек строит модель интерфейса и переносит её на новый экран. Если на одном экране «Сохранить» справа внизу, а на другом слева вверху и называется «Применить», перенос ломается — внимание уходит с задачи на разбор интерфейса. Это измеримо: растёт время выполнения, растёт доля отказов на шаге (см. Метрики UX).

Где выгоды нет. Стартап из трёх человек с непроверенной гипотезой систему строить не должен: он ещё не знает, какие компоненты ему нужны, и потратит недели на абстракции, которые выкинет. Рабочее эмпирическое правило — правило трёх: решение уезжает в систему после третьего независимого повторения, а не после первого «вдруг пригодится». До этого — преждевременная абстракция ровно в том же смысле, что и в коде.

Что забирать первым, удобно решать по двум осям: как часто элемент встречается и насколько дорого обходится расхождение.

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

Слои системы: где живёт решение

Слои дизайн-системы: от токенов до экранов продукта

Главное правило чтения схемы: правку делают на самом нижнем слое, на котором она ещё верна для всех потребителей. «Отступ 12 вместо 16» верно только для одного экрана — правка экрана. Верно для всех форм — правка паттерна. Верно для плотности интерфейса — правка токена, и тогда она перекрасит продукт целиком, что одновременно и хорошо, и опасно. Обратное правило тоже работает: чем ниже слой, тем дороже ошибка. Опечатка в экране живёт до следующего релиза экрана; ошибка в токене color.text.secondary (контраст 3.4:1 вместо 4.5:1) размазывается по тысячам мест и всплывает через полгода на аудите.

Разделение «паттерн против компонента» стоит проговорить отдельно — здесь ошибаются чаще всего. Компонент — это код и макет. Паттерн — это договорённость. «Форма настроек» — паттерн: заголовок группы, поля в один столбец, подпись под полем, панель действий внизу, изменения применяются по кнопке, а не мгновенно. Делать из этого компонент <SettingsForm> почти всегда ошибка: слишком много вариаций, компонент обрастает пропсами и умирает. Паттерн живёт в документации, проверяется на ревью и стоит ноль строк кода.

Токены: именованные решения вместо магических значений

Токен — то же самое, что именованная константа в коде. Вместо #1D4ED8 в тридцати местах — имя, за которым стоит решение. Ценность не в том, что «удобно менять цвет» (это следствие), а в том, что имя фиксирует смысл: color.action.primary.bg говорит, что это фон главного действия, и запрещает красить им декоративную плашку.

Три уровня токенов и разрешение ссылок

  1. Ядро (core, reference, primitive) — сырые значения: blue-600, gray-050, space-4. Никакого смысла, только факт. Это палитра художника, а не инструкция.
  2. Смысловые (semantic, system) — роли: color.text.primary, color.surface.raised, color.feedback.danger.text. Каждый ссылается на ядро. Здесь живут тема и бренд.
  3. Компонентныеbutton.primary.bg, input.border.rest. Нужны только там, где компонент требует исключения из общих ролей. Плодить их «на всякий случай» — верный способ получить тысячу токенов, в которых никто не разберётся.

Почему компонент не должен ссылаться на ядро напрямую. Если в кнопке написано background: blue-600, тёмная тема требует править кнопку. И карточку. И бейдж. И ссылку. Смысловой слой — единственная точка, где тема переопределяется: имена остаются, значения меняются. Это буквально уровень косвенности из инженерии и работает по той же причине.

Именование

Схема, которая доживает до третьего года: категория.роль.вариант.состояниеcolor.text.on-accent, color.action.primary.bg.hover, color.action.danger.border.focus, space.inline.sm, radius.control, elevation.overlay, duration.enter.fast.

Плохое имя Почему Как надо
color.blue Через год он станет зелёным, имя соврёт color.action.primary.bg
color.button-blue-hover-2 Привязка к одному месту, номер без смысла color.action.primary.bg.hover
spacing.medium «Средний» относительно чего? space.stack.md с явной шкалой
text.14 Размер вместо роли; на мобильном другой text.body.sm
color.dark.text Тема зашита в имя — темы не переключаются color.text.primary + карта темы

Про шкалы. Шаг 4 px в отступах — не закон природы, а полезная конвенция: она сокращает пространство решений и делает ритм предсказуемым. Шкала 4, 8, 12, 16, 24, 32, 48, 64 покрывает почти всё; если постоянно нужен «13», проблема обычно не в шкале, а в том, что вы компенсируете чужой внутренний паддинг. Про типографическую шкалу и вертикальный ритм — Основы визуального дизайна.

Формат и пайплайн

Есть открытый формат — Design Tokens Format Module от Design Tokens Community Group: он стандартизует описание токена в JSON вместе со ссылками на другие токены.

{
  "color": {
    "blue": {
      "600": { "$type": "color", "$value": "#1D4ED8" },
      "400": { "$type": "color", "$value": "#60A5FA" }
    },
    "action": {
      "primary": {
        "bg":   { "$type": "color", "$value": "{color.blue.600}" },
        "text": { "$type": "color", "$value": "{color.gray.050}" }
      }
    }
  },
  "space": { "stack": { "md": { "$type": "dimension", "$value": "16px" } } }
}

Дальше один источник превращается в артефакты всех платформ — обычно через Style Dictionary:

// style-dictionary.config.ts — одна правда, много выходов
export default {
  source: ["tokens/**/*.json"],
  platforms: {
    // веб: CSS-переменные, отдельный файл на каждую тему
    css: { transformGroup: "css", buildPath: "dist/css/",
           files: [{ destination: "tokens.css", format: "css/variables" }] },
    // типы для автодополнения и запрета «магических» строк в коде
    ts:  { transformGroup: "js", buildPath: "dist/ts/",
           files: [{ destination: "tokens.ts", format: "javascript/es6" }] },
    // ios и android описываются так же, но со своими единицами измерения
  },
};

На выходе для веба переключение темы становится заменой одной карты соответствий:

:root {
  --color-blue-600: #1d4ed8;
  --color-blue-400: #60a5fa;
  --color-action-primary-bg: var(--color-blue-600);
  --space-stack-md: 16px;
}
/* тёмная тема переопределяет ТОЛЬКО смысловой слой */
[data-theme="dark"] { --color-action-primary-bg: var(--color-blue-400); }
/* компонент не знает ни про палитру, ни про тему */
.btn--primary {
  background: var(--color-action-primary-bg);
  padding-block: var(--space-stack-md);
}

Полная цепочка от решения дизайнера до продакшена:

Ключевой узел здесь — D. Проверки токенов в CI отличают систему от благих намерений. Минимум: контраст всех пар «текст на поверхности» по WCAG 2.2, отсутствие висячих ссылок, запрет прямого использования ядра в компонентах, диф изменений в релизных заметках. Руками это невозможно уже на сотне токенов. Как эти переменные ложатся в реальный CSS и почему каскад важен для тем — Архитектура CSS и Основы CSS.

Типичные ошибки с токенами. Нет смыслового слоя — в макетах и коде gray-700, и тёмная тема превращается в квартальный проект. Токен под каждый экран (color.checkout.summary.bg) — признак: используется ровно один раз. Токены только для цвета — отступы, радиусы, тени, длительности и толщины границ расходятся так же, просто менее заметно. Значение поменяли, имя оставили — половина продукта поехала без строчки в changelog. Токены есть, а линтера нет — разработчик пишет #fff, «потому что быстрее», и это никто не ловит.

Компоненты: API как контракт

Компонент — это обещание: выглядеть определённым образом во всех состояниях, вести себя предсказуемо с клавиатуры, корректно объявлять себя вспомогательным технологиям, не ломаться от длинного текста. Всё, что не обещано, потребитель реализует сам — и система за это не отвечает. Отсюда главный принцип: сначала формулируем, что компонент гарантирует, потом рисуем.

Варианты против булевых флагов

Самый частый способ убить компонент — набор булевых пропсов.

// ПЛОХО: 2^7 = 128 сочетаний, из которых осмысленны штук шесть
interface ButtonProps {
  isPrimary?: boolean; isSecondary?: boolean; isGhost?: boolean;
  isDanger?: boolean;  isSmall?: boolean;     isLarge?: boolean;
  isFullWidth?: boolean;
}
// Что значит isPrimary + isGhost + isDanger одновременно? Никто не знает.
// Дизайнер такого не рисовал, тестов на это нет, но код такое допускает.
// ХОРОШО: невозможные состояния невыразимы
interface ButtonProps {
  /** Уровень акцента. На экране допустима ровно одна кнопка primary. */
  variant?: "primary" | "secondary" | "ghost";
  /** Смысловая окраска действия, а не цвет. */
  tone?: "neutral" | "danger";
  size?: "sm" | "md" | "lg";
  /** Растягивается по контейнеру — нужно на мобильных и в модалках. */
  block?: boolean;
  /** Показывает индикатор и блокирует повторный клик. */
  loading?: boolean;
  /** Причина недоступности обязательна: без неё блокировать нельзя. */
  disabled?: { reason: string } | false;
}

Что здесь важно помимо синтаксиса. Перечисление вместо флагов убирает комбинаторный взрыв: тестировать нужно 3 × 2 × 3 осмысленных сочетаний, а не 128 бессмысленных. tone отделён от variant — иначе появится isDangerGhost, потом isDangerGhostSmall, и система превратится в свалку. disabled требует причину — это дизайнерское решение, зашитое в тип: заблокированная кнопка без объяснения одна из самых частых причин застревания в формах. loading — часть контракта, а не украшение: двойная отправка платежа это баг компонента, а не экрана (подробнее — Взаимодействие и состояния).

Композиция против конфигурации

Второй способ убить компонент — описывать пропсами всё. Признак: появились headerIcon, headerIconColor, headerBadgeText, footerLeftSlotText.

// ПЛОХО: компонент пытается быть конструктором
<Modal title="Удалить проект" titleIcon="warning"
       bodyText="Это действие необратимо." bodySecondaryText="Все отчёты будут удалены."
       primaryButtonText="Удалить" primaryButtonTone="danger"
       secondaryButtonText="Отмена" showCloseIcon />

// ХОРОШО: компонент владеет поведением, содержимое — за потребителем
<Modal onClose={close} labelledBy="del-title">
  <Modal.Header id="del-title">Удалить проект</Modal.Header>
  <Modal.Body><p>Это действие необратимо. Все отчёты будут удалены.</p></Modal.Body>
  <Modal.Footer>
    <Button variant="ghost" onClick={close}>Отмена</Button>
    <Button variant="primary" tone="danger" onClick={remove}>Удалить</Button>
  </Modal.Footer>
</Modal>

Разделение простое: система владеет поведением и структурой, продукт владеет содержимым. Модалка отвечает за перехват фокуса, закрытие по Escape, возврат фокуса на триггер, блокировку прокрутки фона и роль dialog; она не отвечает за то, сколько абзацев внутри. Механика фокуса разобрана в Клавиатуре и фокусе, готовые паттерны — в Компонентах и ARIA.

Разбор 1: пользователь не нашёл кнопку

Экран «Настройки уведомлений»: двенадцать переключателей, внизу кнопка «Сохранить» в варианте ghost — прозрачная, текст цветом color.text.secondary, справа ниже линии сгиба. В тесте четверо из шести переключили тумблеры и ушли со страницы. Изменения не сохранились. Что произошло:

  1. Кнопка не читается как кнопка. У ghost нет ни фона, ни границы; единственный признак — цвет текста, а он приглушённый. Взгляд сканирует страницу на «кликабельные объекты» по форме и контрасту, а не по семантике.
  2. Она вне зоны видимости. Двенадцать строк не помещаются в первый экран, а участники, закончив с тумблерами, считали задачу выполненной.
  3. Ложная модель мгновенного сохранения. Тумблер — паттерн немедленного применения (так работает системный интерфейс телефона). Тумблер плюс кнопка «Сохранить» — конфликт двух моделей.

Чинится это системой, а не косметикой. В документацию попадает правило: на экране ровно одно главное действие, и оно variant="primary"; ghost — только для третьестепенных действий рядом с более сильными. В паттерн «Форма настроек» вписывается: если изменения применяются по кнопке, панель действий закреплена внизу области формы; если закрепить нельзя — переключатели применяются мгновенно, а откат даётся через «Отменить» в тосте. В компонент Switch добавляется правило применения: только для немедленного эффекта, для отложенного — Checkbox. Обратите внимание, чего здесь нет: фразы «сделаем кнопку заметнее». Есть три конкретных ограничения, каждое объясняется задачей пользователя и проверяемо на ревью.

Разбор 2: почему форма неудобна

Форма смены тарифа: одиннадцать полей, все ошибки после нажатия «Продолжить», подписи заменены плейсхолдерами, поле «ИНН» отклоняет ввод с пробелами без объяснения, кнопка отправки заблокирована, пока форма невалидна.

Что видит пользователь Реальная причина Решение на уровне системы
«Не помню, что было в этом поле» Плейсхолдер вместо подписи: текст исчезает при вводе Field не принимает placeholder без label; label обязателен в типе
«Мне показали шесть ошибок сразу» Валидация только на сабмит Паттерн: проверка на blur, повторная — на change после первой ошибки
«Кнопка не нажимается, и непонятно почему» disabled без причины disabled требует reason; паттерн запрещает блокировать сабмит — лучше показать ошибки
«Мой ИНН не принимается» Маска отвергает пробелы из буфера обмена Правило: поля нормализуют вставленное значение, а не отвергают
«Форма бесконечная» Одиннадцать полей без группировки Паттерн формы: группы по 5–7 полей с заголовками, длинные формы делятся на шаги

Принцип: почти каждая «неудобная форма» — это не отсутствие вкуса, а отсутствие паттерна. Когда решение о поведении ошибок принимает каждый автор экрана заново, разброс неизбежен. Техническая сторона — Формы и валидация, формулировки сообщений — Текст в интерфейсе.

Документация: то, без чего система не работает

Недокументированный компонент используют неправильно, а через полгода дублируют, «потому что не нашли». Документация — не приложение к системе, это её половина. Минимальный состав карточки компонента:

  1. Назначение в одном предложении. «Кнопка запускает действие на текущей странице.»
  2. Когда НЕ использовать. «Для перехода на другую страницу — Link, иначе ломается открытие в новой вкладке и Ctrl+клик.» Этот раздел ценнее раздела «когда использовать»: он предотвращает большую часть ошибок применения.
  3. Анатомия — схема с названными частями (контейнер, иконка, подпись, индикатор). Названия должны совпадать в макете и в коде.
  4. Варианты и когда какой — не «есть primary, secondary, ghost», а «primary — главное действие экрана, ровно одно; secondary — альтернативы; ghost — действия в плотных списках».
  5. Состояния: rest, hover, active, focus-visible, disabled, loading, для полей ещё error и read-only. Пропущенное состояние — самый частый дефект передачи.
  6. Поведение: реакция на клавиатуру, порядок фокуса, что происходит при долгом ответе сервера и при переполнении текстом.
  7. Правила контента: максимальная длина подписи, глагол в императиве, запрет «ОК/Отмена» для необратимых действий.
  8. Доступность: роль, что объявляется, контраст, размер цели. Код и макет рядом — живой пример и ссылка на компонент библиотеки. Changelog — что изменилось и что делать при обновлении.

Пары «так / не так» работают лучше правил. Правило «не используйте ghost для главного действия» абстрактно; два скриншота рядом с подписью «пользователи не находят кнопку сохранения» — конкретно. Обязательно объяснение «почему»: без него правило воспринимается как вкусовщина и первым же нарушается.

Кто пишет — автор изменения; документация входит в определение готовности, компонент без карточки не релизится. Если писать доку «потом», её не будет никогда. Как дока не устаревает — единственный надёжный способ генерировать её из живого кода: Storybook рендерит примеры из настоящих компонентов, и при смене API пример падает, а скриншоты в вики устаревают за квартал. Ссылку на живую страницу компонента стоит класть прямо в описание компонента в библиотеке макетов — тогда дизайнер и разработчик смотрят в один источник. Образцы формата, на которых стоит учиться: GOV.UK Design System (лучший в индустрии раздел «когда не использовать» с обоснованиями исследованиями), Shopify Polaris, GitHub Primer, Material Design 3, Apple HIG.

Доступность на этапе дизайна: пять решений в макете

Полностью тема раскрыта в треке доступности — начните с обзора и визуальной доступности; в этом треке ей посвящена статья Доступность на этапе дизайна. Здесь — только то, что решается в системе и после релиза чинится дорого.

  1. Контраст проверяется у пар, а не у цветов. Токен color.text.secondary сам по себе контраста не имеет — контраст есть у пары «текст на поверхности». Поэтому в системе фиксируют допустимые пары и гоняют их в CI: обычный текст ≥ 4.5:1, крупный (от 18.66 px bold или 24 px) ≥ 3:1, границы интерактивных элементов и смысловые иконки ≥ 3:1.
  2. Фокус — это токен, а не «браузерная обводка». color.border.focus и outline-offset задаются один раз на всю систему. Убирать outline без замены нельзя: без видимого фокуса интерфейс непроходим с клавиатуры. WCAG 2.2 добавил критерий 2.4.11: фокус не должен закрываться липкими шапками — а это уже вопрос макета.
  3. Размер цели. WCAG 2.2 требует минимум 24×24 CSS-px (критерий 2.5.8), рекомендации платформ — 44 px у Apple и 48 dp у Android. Это следствие закона Фиттса: время попадания растёт при уменьшении цели. Важно, что цель может быть больше видимого элемента — иконка 20 px с прозрачным паддингом до 44 px допустима, и решается это в компоненте один раз.
  4. Цвет не единственный носитель смысла. Красная рамка поля без текста ошибки и без иконки недоступна. В системе это фиксируется в компоненте: Field с error обязан отрисовать текст ошибки.
  5. Порядок и группировка в макете = структура в коде. Визуальный порядок совпадает с порядком чтения, заголовки — настоящая иерархия, а не «крупный жирный текст». Если в макете два столбца, дизайнер обязан сказать, в каком порядке они читаются на узком экране.

Отдельный частый провал — disabled-состояние: серый на сером даёт контраст около 2:1 и не проходит ни один критерий, а пользователь не понимает причину. Практика лучше: не блокировать, а показывать ошибку при попытке; если блокировать необходимо — держать читаемый контраст и рядом писать причину. Это ровно то, что мы зашили в тип disabled: { reason: string }.

Передача в разработку: что такое нормальный хендофф

Слово «передача» вводит в заблуждение: оно предполагает, что дизайн заканчивается там, где начинается код. На практике это цикл, и чем раньше в нём появляется разработчик, тем дешевле всё остальное.

Что входит в нормальную передачу — не ссылка на макет, а комплект. Список используемых компонентов системы с вариантами (разработчик не должен угадывать, secondary это или ghost). Всё новое, чего в системе нет, явно помечено — с ответом, отклонение это или кандидат в систему. Все состояния: пустое, загрузка, ошибка загрузки, частичная ошибка, нет прав, нет данных за период, слишком много данных; макет только «счастливого пути» — половина работы, выданная за целую. Правила адаптива вместо трёх макетов на три ширины: «карточки перестраиваются в один столбец, когда ширина колонки меньше 280 px» — правило переносится в код, три картинки нет. Крайние случаи контента: имя из 60 символов, ноль элементов, 10 000 элементов, отсутствующая аватарка, отрицательное число, немецкий перевод (обычно +30% к длине). Поведение и время: что происходит между кликом и ответом, при каком времени показывается скелетон, что при ошибке сети. И что можно менять: «эти отступы кратны 8, точное значение на ваше усмотрение».

Почему «пиксель в пиксель» — плохая цель

  1. Макет — статичный срез одного состояния при одной ширине с одними данными. Продукт — множество состояний при произвольных ширинах и данных. Требование точного совпадения определено только для нарисованного среза, то есть для исчезающе малой доли реальных случаев.
  2. Технически точное совпадение недостижимо. Рендеринг шрифта отличается между Windows и macOS; line-height даёт разные метрики в зависимости от шрифтовых таблиц; субпиксельное сглаживание, системное масштабирование 125%, минимальный размер шрифта в браузере, prefers-reduced-motion — всё это меняет картинку. Редактор макетов рисует текст своим движком, браузер своим.
  3. Стоимость последней мили несоразмерна. Довести совпадение с 95% до 99% часто дороже, чем реализовать три пропущенных состояния. Команда полирует то, что видно на скриншоте, и не делает того, что видят пользователи.
  4. Требование ломает адаптивность. Чтобы совпало точно, разработчик фиксирует размеры. Фиксированные размеры ломаются при увеличении шрифта, длинном тексте и другой локали — то есть ровно там, где живут реальные пользователи.
  5. Это портит отношения. Дизайнер в роли «QA пикселей» и разработчик в роли «исполнителя макета» — конфигурация, в которой оба несчастны и никто не отвечает за результат.

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

Обязано совпадать Допустимо расходиться
Токены: цвет, шрифт, шаг шкалы отступов, радиус Итоговый рендер тени на 1–2 px
Визуальная иерархия: что главное, что второстепенное Точная высота строки после рендера шрифта
Все состояния и переходы между ними Отступ 22 против 24, если оба из шкалы и ритм сохранён
Поведение с клавиатуры, порядок фокуса Кривая анимации в рамках токена длительности
Тексты, включая ошибки и пустые экраны Ширины, не описанные правилом адаптива
Правила перестроения при изменении ширины Реализация внутренней разметки

Практическая формулировка для команды: «Отклонение — это когда сломан токен, состояние или иерархия. Отклонение — это не два пикселя». Ревьюить реализацию нужно не наложением скриншотов, а по чек-листу: пройти все состояния, пройти клавиатурой, вставить длинный текст, сузить до 320 px, увеличить шрифт до 200%, включить тёмную тему. Регрессию дешевле автоматизировать на уровне компонентов системы, а не страниц: скриншот страницы падает от любой правки данных, скриншот компонента стабилен (Тестирование фронтенда, E2E и UI-тесты). И договоритесь о единицах: дизайнер работает в px при масштабе 1×, разработчик переводит шрифты и вертикальные отступы в rem, чтобы уважалась системная настройка размера шрифта. «В макете 14 px, в коде 0.875rem» — не расхождение, а правильная реализация.

Жизнь системы: версии, депрекейшн, миграции

Система, которую нельзя менять, умирает так же надёжно, как система, которую меняют без правил. Нужен явный жизненный цикл.

Ключевой момент — статус Experimental. Без него любое добавление становится пожизненным обязательством, и команда начинает бояться добавлять. Экспериментальный компонент брать можно, но его API может поехать в минорной версии, и это заранее известно всем.

Что считать ломающим изменением. Семантическое версионирование применимо к UI, но «ломающее» здесь шире: сломать можно не только компиляцию, но и визуальное решение.

Изменение Версия Почему
Добавлен вариант variant="link" minor Старый код работает
Переименован проп typevariant major Компиляция ломается
Изменено значение color.action.primary.bg major, если меняется бренд-решение Перекрашивает продукт молча
Исправлен контраст color.text.secondary minor + громкое объявление Формально визуальная правка, но обязательная
Изменён дефолт size с md на sm major Молча меняет все существующие экраны
Удалён вариант ghost major, только после депрекейшна Ломает экраны
Анимация ускорена с 300 до 200 мс patch Незаметно, не ломает раскладку

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

Рабочая политика депрекейшна состоит из четырёх обязательных частей. Замена существует и задокументирована — помечать устаревшим то, на что нечем заменить, значит получить вечный @deprecated. Предупреждение видно там, где работают: warning в dev-сборке, пометка в библиотеке макетов, запись в changelog — три канала, потому что дизайнер не читает консоль, а разработчик не открывает Figma. Автоматическая миграция, где возможно: переименование пропа — это кодмод, а не задача на двадцать человеко-дней; написать кодмод дешевле, чем уговаривать четыре команды. Срок и владелец удаления: «удаляем в v3.0.0, ориентировочно через два квартала» — конкретно, «когда-нибудь уберём» — нет.

# кодмод переименовывает проп во всех продуктовых репозиториях
npx jscodeshift -t ./codemods/button-type-to-variant.ts \
  --extensions=tsx,ts --parser=tsx apps/*/src
# и проверяем, что старых использований не осталось
rg -n 'Button[^>]*\stype="(primary|secondary)"' apps/ && echo "остались" || echo "чисто"

Если продукты живут в монорепозитории, миграция радикально дешевеет: один PR меняет систему и всех потребителей атомарно (компромиссы такого устройства — Монорепозиторий).

Метрики и владение

Без чисел разговор про систему превращается в спор о вкусах. Adoption — доля компонентов на экранах, взятых из системы; считается статическим анализом импортов, а не на глаз, и полезнее по ключевым экранам, чем по всему коду. Число отклонений — сколько мест переопределяют стили системы (!important, локальные переопределения, отвязанные компоненты в макетах); растёт — значит, системе чего-то не хватает, и это подсказка, что добавлять. Время от решения до продакшена — изменили токен, через сколько дней он у всех? Это главная метрика механизма распространения. Доля команд на последней мажорной версии: если половина сидит на предыдущей, вы поддерживаете две системы. Опасность любой метрики — превращение в цель (закон Гудхарта): adoption 100% достигается запретом на отклонения, а вместе с ним запретом на эксперименты. Здоровое значение обычно 70–90% с живым потоком RFC.

Модель владения Как устроена Когда работает
Централизованная Выделенная команда, все изменения через неё Малый масштаб; на росте становится узким местом с очередью
Федеративная Команды системы нет, правят представители продуктов Почти всегда деградирует: у общего блага нет хозяина
Гибридная Небольшое ядро владеет токенами, инфраструктурой и правом вето; вклад продуктов по RFC От 20–30 человек в разработке и выше; требует процесса и дисциплины

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

Где закономерность, а где вкус

Честность в этом вопросе — признак зрелости. Часть решений опирается на исследования и проверяемые ограничения, часть — на стиль; смешивать их вредно, потому что первые нельзя нарушать без последствий, а вторые нельзя навязывать под видом истины.

Есть данные и ограничения — спорить можно, но с цифрами. Контраст текста и интерфейсных элементов: WCAG 2.2, пороги 4.5:1 и 3:1 — измеримое требование, во многих юрисдикциях ещё и юридическое. Размер и расстояние до цели: закон Фиттса, минимум 24 CSS-px, платформенные 44/48. Пороги времени отклика: до 0.1 с воспринимается как мгновенное, до 1 с не прерывает мысль, после 10 с внимание уходит — классические пороги Нильсена, восходящие к работам Миллера 1968 года. Видимый фокус необходим для работы с клавиатуры — проверяется за пять минут. Ошибка рядом с полем находится быстрее, чем в общем блоке наверху, — воспроизводится в любом юзабилити-тесте. Иконка без подписи надёжно узнаётся только для нескольких десятилетиями закреплённых символов (поиск, закрыть, печать).

Вопрос стиля и бренда — решает владелец бренда, спорить бессмысленно. Радиус скругления 0, 4, 8 или 16 — вопрос характера продукта, а не удобства. Плотность интерфейса (воздушная против компактной) зависит от задачи — админка для операторов и лендинг разные, — но внутри разумного диапазона это вкус. Конкретный акцентный оттенок при равном контрасте, шрифтовая пара, тени против плоскости, стиль иллюстраций, тон анимации. Порядок «Отмена / Подтвердить» — платформенная конвенция, а не универсальная истина; важно быть последовательным внутри продукта.

Отсюда рабочий приём в споре: перевести утверждение в проверяемое или признать его стилем. «Эта кнопка теряется» превращается либо в «контраст границы 2.1:1 при требуемых 3:1», либо в «мне кажется, слишком бледная». Первое чинится измерением, второе решается владельцем стиля за пять секунд. Половина бесконечных дизайн-ревью — это споры, где обе стороны не заметили, что обсуждают разные категории. И отдельно про карго-культ: «в Material так» не аргумент — Material решает задачи Google на их масштабе и платформе. Заимствовать чужие решения полезно, заимствовать чужие ограничения нет; читайте обоснования в чужих системах, а не только картинки.

Типичные ошибки

  • Система как разовый проект. Собрали за квартал, отпраздновали, распустили команду — через полгода это ещё один легаси-пакет.
  • Начали с компонентов, а не с токенов. Компоненты на магических значениях переписываются целиком при первой же теме.
  • Слишком рано и слишком абстрактно. <UniversalCard> с 24 пропсами, которым никто не пользуется, потому что проще написать свой div.
  • Дока отдельно от кода. Расходятся за один спринт.
  • Нет процесса вклада. Команда, которой не хватило варианта, форкает компонент; через год форков больше, чем компонентов.
  • Adoption как самоцель. Запрет на отклонения убивает поток обратной связи, из которого система растёт.
  • Дизайнер как контролёр пикселей. Ревью превращается в тридцать пунктов по 2 px, а пропущенное состояние ошибки никто не замечает.
  • Молчаливые изменения. Поменяли дефолт или значение токена без мажорной версии — доверие теряется мгновенно и восстанавливается годами.
  • Система без бюджета на поддержку. Пока владельцу некогда отвечать на вопросы и ревьюить RFC, командам дешевле обойти систему, чем договориться с ней.

Мини-итог

  • Дизайн-система = решения + механизм распространения + договорённость о владении. Без любой из трёх частей это набор картинок.
  • Токены — именованные решения. Три уровня: ядро, смысловой, компонентный; компоненты ссылаются на смысловой слой — только так работают темы и бренд. Проверки токенов в CI отличают систему от намерений.
  • Компонент — контракт: перечисления вместо булевых флагов, композиция вместо конфигурации, поведение и доступность внутри, содержимое снаружи.
  • Паттерн — не компонент. Многие «неудобные формы» лечатся документированной договорённостью, а не кодом.
  • Документация обязательна, и раздел «когда НЕ использовать» — самый полезный в ней.
  • Доступность решается в макете: пары контраста, фокус как токен, размер цели, не только цвет, порядок чтения. Подробности — в треке доступности.
  • Хендофф — цикл, а не бросок через стену. Совпадать должны токены, иерархия, состояния и поведение; «пиксель в пиксель» крадёт время у состояний и адаптива.
  • У системы есть версии, депрекейшн со сроком, кодмоды и метрики принятия; молчаливые изменения дефолтов убивают доверие.
  • Разделяйте закономерности и стиль: первое проверяется числом, второе решает владелец бренда — обсуждать их одинаково нельзя.

Источники

Что дальше

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

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов