Кнопка начинает расходиться не потому, что в ней много CSS. Обычно один код владеет className, другой — disabled, третий — текстом, а четвёртый копирует цвет. Симптом проявляется после безопасной на вид правки: новая loading-версия смотрится правильно, но action уже доступен для повторного запуска; focus ring пропадает в одном варианте; иконка получает label только в profile. Цена — review видит фрагменты, а пользователь получает разный contract для одного знакомого действия.
Минимальный component contract собирает эти фрагменты в данные: token names отвечают за значения, required states — за допустимые ветки, semantic fields — за смысл control, usage inventory — за известный радиус изменения. Это не универсальная система и не готовая React API. В учебном скрипте нет JSX, DOM, CSS cascade и assistive technology. Он лишь показывает, как проверить, что один договор не пропустил обязательную часть до того, как команда начнёт спорить о структуре библиотек.
Четыре владельца вместо одного большого объекта
У contract есть четыре слоя. Первый — token layer: он знает только именованные значения и не должен решать, в каком состоянии находится кнопка. Второй — state layer: default, hover, focus-visible, disabled и loading; он определяет, какие ветки продукт обязан обсудить. Третий — semantic layer: name, role, state и disabled. Он не выводится из цвета, потому что одинаковый серый может означать disabled, loading или ошибочно применённый style. Четвёртый — inventory: он хранит известные точки применения и не выдаёт себя за поиск по всему репозиторию.
| Слой | Владеет | Не доказывает | Нужная внешняя проверка |
|---|---|---|---|
| Tokens | имена и значения background, foreground, focus ring, radius, gap | что все pixels в браузере обновились | собранный CSS и visual diff в выбранной среде |
| States | разрешённые default/hover/focus-visible/disabled/loading | что browser реально получил hover или focus | ручной keyboard/mouse scenario или автоматизация |
| Semantics | declared name, role button, declared state | что screen reader произнёс ожидаемую фразу | проверка DOM/accessibility tree и выбранной технологии |
| Inventory | три явно перечисленных usage | что больше usage не существует | поиск в кодовой базе и review migration |
| Visual payload | viewports, states и token names для будущего снимка | реальный screenshot, diff, score или regression | настоящий visual-regression runner с сохранённым артефактом |
Почему CSS variable не является semantic token автоматически
Спецификация CSS Custom Properties говорит о custom properties и подстановке var(). Она не назначает им смысл. Поэтому --button-primary-background может быть технически валиден, но неясен как договор, если никто не определил, для какой роли он существует, кто меняет его значение и какие variants от него зависят. Обратная ошибка тоже частая: назвать произвольное значение «semantic» и считать, что оно должно жить в глобальном файле. Семантика появляется не в пунктуации имени, а в повторяемом решении и известном owner.
Для маленькой системы полезен направленный путь: button.primary.background → primary button → конкретные usage. Он позволяет отдельно решить, как component получает CSS variable. В одном коде это может быть custom property, в другом объект theme, в третьем stylesheet. Contract не требует выбрать транспорт заранее. Он требует не потерять связь между ролью значения и точками, на которые повлияет изменение. Такая граница оставляет миграцию обратимой.
Name, role и state — не оформление
WAI-ARIA 1.2 описывает роли, состояния и свойства для интерфейсных объектов. Для простой кнопки лучший путь обычно начинается с native button: браузер уже знает базовую keyboard-модель. Если вместо него нужен custom host, команда обязана явно воспроизвести поведение, а не только написать role="button". Однако даже native host не решает вопрос имени: «Сохранить изменения» и иконка без text alternative не равны для человека, который не видит layout.
В модели semantic contract намеренно мал: name, role, state, disabled и список permitted states. Поле state — declared data, не прочитанное browser tree. Такое различие нужно сохранить в тексте review: fixture может показать, что state loading предусмотрен в contract, но не может сказать, что конкретный browser запретил повторный click или что screen reader сообщил disabled. Для этого понадобятся платформенные тесты, которых здесь нет.
Исполнимый пример: проверить contract без UI
Функция checkContract сопоставляет required states с permitted states, проверяет, что payload не ссылается на отсутствующий token, и что каждое известное usage имеет name и роль button. Она не ищет реальные файлы и не запускает построение styles. Именно поэтому результат true означает «данные учебной модели согласованы», а не «кнопка доступна и визуально одинакова». Это узкое утверждение легко повторить и трудно неверно истолковать.
const system = createButtonSystem();
const review = checkContract(system);
console.log(review.ok); // true
console.log(system.contract); // { name, role, state, disabled, permittedStates }
// Никакой button в DOM не создан; поля описывают проектный contract.
Маршрут: симптом → причина → проверка → действие
- Симптом. Запишите один разрыв: кнопка теряет focus, loading не блокирует повтор, label отличается в одинаковом действии или literal color появился в новом usage.
- Причина. Разложите изменение по четырём владельцам. Если token пытается хранить state, а CSS class несёт name, граница уже размыта.
- Проверка contract. Сверьте required states, token names, semantic fields и inventory. Запустите
--verify-fixture; он должен отклонить undeclared token и неверное значение. - Проверка платформы. Отдельно создайте реальный control и пройдите согласованный keyboard/mouse/a11y сценарий. Результат сохраните как новый артефакт, не внутри model.
- Действие. Сначала поменяйте один owner: вынесите literal в named token, добавьте state или верните native host. Не объединяйте это с переписыванием всей библиотеки.
- Откат. Применяйте token correction с сохранённым previous value. Если inventory показывает неожиданный effect, откатите малый change и сузьте variant.
Против ложной универсальности
Слово «Button» не делает все действия одним компонентом. Link-like navigation, destructive confirmation, toggle, split button и async submit имеют разные риски. У них могут совпадать radius и gap, но не обязательно name, behavior или state matrix. Универсальный API, который принимает двадцать optional props ради такого сходства, обычно скрывает больше решений, чем экономит. Малый contract ценнее, когда он допускает честный ответ: этот control пока не входит в primary button.
Не стоит и использовать fixture как gate для чужого продукта. Её inventory полностью создан внутри примера, а values выбраны для объяснения. В настоящем проекте сначала нужно получить существующие usage и владельца правила. Затем выбрать, какие values public, какие variants поддерживаются, как маркируется deprecation и кто проводит visual review. Это следующий слой T-shape автора 2022 года: не говорить за процесс, которого не наблюдали, а назвать факт, которым можно проверить изменение.
Ограничение и следующий проверяемый шаг
Модель не меряет contrast, не запускает CSS, не сравнивает image pixels, не читает accessibility tree и не знает всех компонентов в архиве. WCAG 2.1 не разрешает заменить сочетание автоматической и ручной оценки полем role в JavaScript. Поэтому в материале нет claim о доступности или visual stability. Здесь есть только контракт данных, который уменьшает шанс забыть state или нечаянно изменить token без пути назад.
Следующий проверяемый шаг — выбрать один production-like component в отдельном репозитории, составить его реальный inventory и записать маленький test plan: browser/version, viewport, states, keyboard route и ожидаемый результат. После первого наблюдения можно привязать к contract настоящий screenshot или accessibility-tree artifact. До этого visual payload должен оставаться честным списком входов, а не «зелёным» score.
Историческая граница мая 2022
Здесь использованы датированные версии, существовавшие до мая 2022 года: CSS Custom Properties CR Draft 11.11.2021 и WAI-ARIA 1.2 CR Draft 08.12.2021; WCAG 2.1 Recommendation опубликована в 2018-м. Фразы о native button и roles относятся к нормативным моделям документов. Слои contract, token names и fixture — решения этого учебного пакета, не цитата из существующей библиотеки.
Проверяемые источники
- CSS Custom Properties for Cascading Variables Module Level 1, Candidate Recommendation Draft от 11 ноября 2021 года — датированный нормативный снимок: custom properties имеют имена --* и подставляются через var(). Он не определяет taxonomy дизайн-токенов и не подтверждает результат visual regression.
- WAI-ARIA 1.2, Candidate Recommendation Draft от 8 декабря 2021 года — датированный нормативный снимок, доступный в мае 2022 года. Он описывает роли и состояния, но не выбирает за продукт токены, текст кнопки или набор variants.
- Web Content Accessibility Guidelines 2.1, Recommendation от 5 июня 2018 года — неизменяемая W3C Recommendation с проверяемыми критериями, включая Keyboard, Focus Visible, Name/Role/Value и Non-text Contrast. Fixture пакета их не измеряет.