Компоненты и состояния: кнопки и поля, которые ведут себя одинаково
Анатомия кнопки, три вида в шести состояниях, восемь состояний и что они показывают, поле с ошибками и настоящие состояния, которые можно попробовать, — с живыми примерами.
Коротко
Компонент в дизайн-системе — это не одна картинка, а набор состояний: кнопка выглядит по-разному, когда над ней курсор, когда на неё перешли с клавиатуры, когда её нажимают, когда она недоступна и пока идёт загрузка. У поля свои состояния: пустое, заполненное, в фокусе, с ошибкой, подтверждённое, недоступное. Если в макете есть только обычное состояние, остальные придумывает разработчик — и каждый компонент в итоге ведёт себя по-своему. Законченный компонент описывает анатомию, варианты и все состояния значениями из токенов, поэтому выглядит и ведёт себя одинаково на любой странице.
Анатомия кнопки
До состояний — части. У каждой части есть значение из токенов, а не число, подобранное для одной этой кнопки.
- Контейнер: фон color.accent-strong, высота 48 px
- Поля по бокам: space.5 = 24 px
- Подпись: font.step-0, насыщенность 600
- Промежуток до значка: space.2 = 8 px
- Значок: 20 px, цвет подписи
- Радиус: radius.full — полностью скруглённые края
Три вида × шесть состояний
Вся кнопка на одном листе. Именно это должно быть в макете до начала разработки.
| Обычное | Наведение | Фокус | Нажатие | Недоступна | Загрузка | |
|---|---|---|---|---|---|---|
| Главная | Купить | Купить | Купить | Купить | Купить | Купить |
| Второстепенная | Купить | Купить | Купить | Купить | Купить | Купить |
| Контурная | Купить | Купить | Купить | Купить | Купить | Купить |
Восемнадцать ячеек вместо трёх кнопок. Разница не в количестве рисования, а в количестве вопросов, которые разработчику не придётся задавать: что происходит при наведении, как показан фокус, как выглядит недоступная контурная кнопка.
Восемь состояний и что они должны показывать
Каждое состояние отвечает на вопрос человека: можно ли это нажать, сработало ли, что не так.
| Состояние | Когда | Что меняется |
|---|---|---|
| Обычное | элемент готов к работе | базовый вид |
| Наведение | над ним курсор | немного другой фон |
| Фокус | на него перешли с клавиатуры | хорошо заметное кольцо |
| Нажатие | в момент клика | темнее тон и лёгкий сдвиг |
| Недоступно | действие пока невозможно | приглушённый вид, нет отклика |
| Загрузка | действие выполняется | индикатор и подпись о процессе |
| Ошибка | значение неверное | сигнальный цвет и подсказка, как исправить |
| Успех | значение проверено | спокойное подтверждение |
Поле ввода в шести состояниях
Ошибка не просто красит рамку — она говорит, что не так и как это исправить.
Попробуйте: настоящие состояния
Подвигайте курсор, нажмите Tab, кликните. В поле введите адрес без домена и уйдите из него — ошибка появится только после того, как вы что-то ввели.
Состояния в коде: 2 примера
Кнопка, где каждое состояние берёт значения из токенов, и разметка загрузки и ошибок, понятная экранным читалкам.
Кнопка и её состояния
Одна строка на состояние; ни один цвет не записан напрямую.
/* Кнопка: описано каждое состояние, каждое значение — из токенов */
.button {
display: inline-flex;
align-items: center;
gap: var(--space-2);
min-height: 2.75rem; /* 44px: удобная цель для пальца */
padding-inline: var(--space-5);
border: 0;
border-radius: var(--radius-full);
background: var(--color-accent-strong);
color: var(--color-on-accent-strong);
font: 600 var(--step-0) / 1 var(--font-text);
transition: background-color var(--duration-fast), transform var(--duration-fast);
}
.button:hover { background: var(--color-accent-strong-hover); }
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }
.button:active { transform: translateY(1px) scale(0.98); }
.button:disabled { background: var(--color-disabled); color: var(--color-text-muted); cursor: not-allowed; }
.button[aria-busy="true"] { cursor: progress; }
Загрузка и ошибки в разметке
aria-busy и aria-describedby сообщают вспомогательным технологиям то, что видит глаз.
<!-- Загрузка: кнопка занята, её нельзя нажать дважды, и она говорит, что происходит -->
<button class="button" type="submit" disabled aria-busy="true">
<span class="spinner" aria-hidden="true"></span>
Сохраняем…
</button>
<!-- Ошибка принадлежит своему полю, и поле на неё ссылается -->
<label for="email">Электронная почта</label>
<input id="email" type="email" aria-invalid="true" aria-describedby="email-error">
<p id="email-error" class="field-error">Проверьте адрес: после @ нужен домен</p>
7 правил законченного компонента
-
01
Все состояния — в макете
Компонент без состояний — эскиз, а не компонент.
-
02
Фокус виден всегда
Кольцо, заметное на любом фоне, — для тех, кто работает с клавиатуры.
-
03
Наведение — не единственный сигнал
На телефоне курсора нет; элемент должен выглядеть нажимаемым и без него.
-
04
44 px для пальца
Нажимаемые элементы — не ниже 44 px, даже если выглядят меньше.
-
05
Ошибки говорят, что делать
«Проверьте адрес: после @ нужен домен», а не «Неверное значение».
-
06
Не только цвет
У ошибки и успеха есть ещё текст или значок — для тех, кто не различает цвета.
-
07
Значения из токенов
Каждое состояние использует роли, поэтому новая тема не ломает ни одно из них.
Частые ошибки с компонентами
-
Только обычное состояние
Каждый разработчик придумывает наведение и фокус по-своему.
-
Убранная рамка фокуса
outline: none без замены делает сайт непригодным для работы с клавиатуры.
-
Недоступная кнопка без объяснения
Человек не понимает, что сделать, чтобы она заработала.
-
Ошибки до ввода
Форма, которая красная ещё до ввода, отпугивает.
-
Нет состояния загрузки
Человек нажимает ещё раз и отправляет заказ дважды.
-
Копии вместо вариантов
Пять слегка разных кнопок вместо одной с тремя вариантами.
Вопросы о компонентах и состояниях
Какие состояния есть у кнопки?
Обычное, наведение, фокус, нажатие, недоступность и загрузка; иногда ещё выбранное.
Чем :focus отличается от :focus-visible?
:focus-visible показывает кольцо, когда человек работает с клавиатуры, и не показывает его после клика мышью.
Когда показывать ошибки в форме?
После того как человек ввёл значение и ушёл из поля или после попытки отправить — :user-invalid делает ровно это.
Нужны ли вообще недоступные кнопки?
Иногда лучше оставить кнопку активной и объяснить проблему по нажатию.
Какого размера должны быть нажимаемые элементы?
Не меньше 44 × 44 px нажимаемой области, даже если видимый элемент меньше.
Сколько вариантов кнопки нужно системе?
Обычно три: главная, второстепенная и контурная — плюс опасное действие там, где оно нужно.
Форма
Компоненты
без догадок
Проектирую и верстаю библиотеки компонентов, где описано каждое состояние, — и в макете, и в коде. Расскажите о проекте — отвечу в течение рабочего дня.