Как делать11 октября 2026 г.9 мин чтения
UX/UI-дизайн: от исследования до дизайн-системыШаг 5 из 9

Анатомия UI-компонента: кнопка, поле, карточка и их состояния

Коротко

UI-компонент — повторяемый элемент интерфейса с заранее заданными частями, вариантами и состояниями: кнопку, поле или карточку описывают один раз так, чтобы их можно было сверстать без догадок. Для каждого компонента определяют анатомию (из каких частей он состоит), варианты (primary, secondary, размеры) и все состояния — default, hover, focus, active, disabled, loading и error. Состояние, которого нет в макете, всё равно появится в продукте, только решать, как оно выглядит, будет тот, кто верстает.

ОсновыУровень: НачальныйUI-компоненты

После материала вы сможете

  • разложить кнопку, поле ввода и карточку на части: контейнер, подпись, иконка, вспомогательный текст, действия
  • перечислить для компонента все состояния — default, hover, focus-visible, active, disabled, loading, error — и описать, что меняется в каждом
  • составить матрицу «вариант × состояние» и спецификацию компонента для передачи в разработку
  • объяснить, почему фокус нельзя прятать, а placeholder не заменяет подпись поля

Проверено на

HTML + CSSMicrosoft Edge 155Playwright 1.64

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Компонент как договор между дизайном и кодом
  2. 02Анатомия: контейнер, подпись, иконка, вспомогательный текст
  3. 03Варианты и размеры: primary, secondary, ghost; S/M/L
  4. 04Состояния: hover, focus-visible, active, disabled, loading, error
  5. 05Поле ввода: label, placeholder, подсказка, ошибка
  6. 06Карточка: кликабельная область и вложенные действия
  7. 07Спецификация компонента для передачи в разработку

Компонент как договор между дизайном и кодом

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

Для дизайнера компонент — мастер-элемент в макете: в Figma он так и называется, component, а его копии на экранах — instances. Для разработчика это код с параметрами, например <Button variant="primary" size="m">. Договор работает, когда обе стороны называют одно и то же одинаково: те же варианты, те же состояния, те же имена свойств. Набор компонентов называют UI-kit, а UI-kit вместе с токенами, правилами и документацией — дизайн-системой.

Анатомия: контейнер, подпись, иконка, вспомогательный текст

Анатомия — это разбор компонента на именованные части. Названия нужны не ради терминологии: когда в спецификации написано «в состоянии error меняется только рамка контейнера», разработчик точно знает, какое свойство трогать. Apple в Human Interface Guidelines описывает кнопку тремя атрибутами: стиль (размер, цвет, форма), содержимое (иконка, текст или оба) и роль (смысл действия: основное, отмена, удаление).

Контейнер

Кнопка
Фон, рамка, радиус, внутренние отступы
Поле ввода
Рамка или подложка, высота, радиус
Карточка
Фон, рамка или тень, отступы, область клика

Подпись

Кнопка
Глагол действия: «Записаться», «Сохранить»
Поле ввода
Label над полем, видна всегда
Карточка
Заголовок — главная ссылка карточки

Иконка

Кнопка
Необязательна, слева или справа от текста
Поле ввода
Необязательна: поиск, показать пароль, очистить
Карточка
Изображение или иконка сверху

Вспомогательный текст

Кнопка
Нет: пояснение ставят рядом с кнопкой
Поле ввода
Подсказка и сообщение об ошибке под полем
Карточка
Описание и мета: цена, дата, теги

Действия

Кнопка
Сама кнопка и есть действие
Поле ввода
Иногда кнопка внутри поля («Очистить»)
Карточка
Основное — переход, второстепенные — отдельные кнопки
Одни и те же части у трёх базовых компонентов

Размер области нажатия — тоже часть анатомии. Apple рекомендует для кнопки область не меньше 44×44 pt. WCAG 2.2 в критерии 2.5.8 (уровень AA) требует, чтобы цель для указателя была не меньше 24×24 CSS-пикселей, с оговорёнными в стандарте исключениями. Видимая часть может быть компактнее, но кликабельная область — нет.

Варианты и размеры: primary, secondary, ghost; S/M/L

Вариант — заранее определённая версия компонента для действий разной важности. У кнопки обычно три-четыре уровня заметности:

Primary

Как выглядит
Заливка фирменным цветом
Когда использовать
Главное действие экрана: «Записаться», «Оплатить»

Secondary

Как выглядит
Контур или светлая подложка
Когда использовать
Альтернатива рядом с главным: «Позвонить», «Подробнее»

Ghost (текстовая)

Как выглядит
Только текст, подложка появляется при наведении
Когда использовать
Третьестепенное: «Отмена», «Пропустить»

Destructive

Как выглядит
Красный акцент
Когда использовать
Удаление и другие необратимые действия

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

Размеры (S, M, L) меняют высоту, внутренние отступы и кегль, но не смысл. Их удобно брать из той же шкалы отступов, что и весь интерфейс (о шкале — в уроке про визуальную иерархию). Для основного действия на телефоне выбирайте размер, у которого область нажатия не меньше рекомендованной платформой.

Состояния: hover, focus-visible, active, disabled, loading, error

Состояние — то, как компонент выглядит и ведёт себя в данный момент. Для пользователя это ответ на вопрос «меня поняли?». Nielsen Norman Group советует не задерживать отклик: фокус и нажатие должны отображаться примерно за 100–150 мс, иначе люди жмут повторно.

Default

Когда
Компонент доступен
Что меняется
Базовый вид
Нюанс
Подпись контрастна к фону

Hover

Когда
Курсор над элементом
Что меняется
Фон темнеет или появляется подложка
Нюанс
На сенсорных экранах наведения нет — смысл за ним не прячут

Focus-visible

Когда
Фокус с клавиатуры
Что меняется
Контур вокруг компонента
Нюанс
WCAG 2.4.7: фокус должен быть виден; контур — не меньше 3:1 к соседнему цвету

Active

Когда
Момент нажатия
Что меняется
Фон ещё темнее, лёгкий сдвиг
Нюанс
Apple: у собственной кнопки состояние нажатия обязательно

Disabled

Когда
Действие сейчас недоступно
Что меняется
Приглушённые фон и подпись
Нюанс
Не фокусируется; причину объясняют рядом

Loading

Когда
Действие выполняется
Что меняется
Спиннер, подпись остаётся
Нюанс
Повторные нажатия игнорируются, результат объявляется

Error

Когда
Значение неверно или действие не удалось
Что меняется
Рамка и текст ошибки
Нюанс
Цвет — не единственный признак
Состояния кнопки и поля: что меняется и на что обратить внимание

Про фокус. Псевдокласс :focus срабатывает при любом фокусе, а :focus-visible — только когда браузер решает, что фокус нужно показать: при переходе клавишей Tab — да, при клике мышью по кнопке — обычно нет. Это позволяет дать клавиатуре заметный контур и не мешать им тем, кто кликает. Убирать индикатор совсем нельзя: MDN прямо предупреждает, что без него навигация с клавиатуры становится недоступной для зрячих пользователей.

Про disabled. Атрибут disabled у <button> браузер обрабатывает сам: кнопка не нажимается и выпадает из порядка Tab. Критерий WCAG 1.4.11 освобождает неактивные компоненты от требования контраста 3:1, но подпись всё равно должна читаться. Если недоступная кнопка должна оставаться в фокусе, используют aria-disabled="true" — тогда блокировать нажатие и стилизовать состояние придётся самостоятельно.

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

index.html
<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Состояния кнопки</title>
  <link rel="stylesheet" href="button.css">
</head>
<body>
  <form id="booking">
    <button class="btn btn--primary" type="submit">Записаться</button>
    <button class="btn btn--secondary" type="button">Позвонить</button>
    <button class="btn btn--ghost" type="button">Отмена</button>
    <button class="btn btn--primary" type="button" disabled>Нет свободных окон</button>
    <p id="status" role="status"></p>
  </form>

  <script>
    const form = document.getElementById("booking");
    const submit = form.querySelector('[type="submit"]');
    const status = document.getElementById("status");

    form.addEventListener("submit", async (event) => {
      event.preventDefault();
      if (submit.getAttribute("aria-busy") === "true") return; // уже отправляем
      submit.setAttribute("aria-busy", "true");
      status.textContent = "Отправляем заявку…";
      await new Promise((resolve) => setTimeout(resolve, 1500)); // вместо запроса
      submit.removeAttribute("aria-busy");
      status.textContent = "Заявка отправлена";
    });
  </script>
</body>
</html>
Разметка и скрипт отправки; вместо реального запроса — пауза 1,5 секунды
button.css
:root {
  --brand: #1d4ed8;
  --brand-hover: #1e40af;
  --brand-active: #1e3a8a;
  --brand-tint: #eff6ff;
  --on-brand: #ffffff;
  --muted-bg: #e5e7eb;
  --muted-text: #4b5563;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 20px;
  border: 1px solid transparent;
  border-radius: 10px;
  font: 600 16px/1.2 system-ui, sans-serif;
  cursor: pointer;
  transition: background-color 150ms ease-out, transform 100ms ease-out;
}

/* Варианты: один и тот же компонент, разная заметность */
.btn--primary { background: var(--brand); color: var(--on-brand); }
.btn--secondary { background: transparent; color: var(--brand); border-color: currentColor; }
.btn--ghost { background: transparent; color: var(--brand); }

/* Hover и active — только у доступной кнопки */
.btn--primary:not(:disabled, [aria-busy="true"]):hover { background: var(--brand-hover); }
.btn--secondary:not(:disabled, [aria-busy="true"]):hover,
.btn--ghost:not(:disabled, [aria-busy="true"]):hover { background: var(--brand-tint); }
.btn--primary:not(:disabled, [aria-busy="true"]):active { background: var(--brand-active); }
.btn:not(:disabled, [aria-busy="true"]):active { transform: translateY(1px); }

/* Фокус с клавиатуры: контур не меньше 3:1 к фону страницы */
.btn:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 2px;
}

/* Disabled: приглушён, но подпись читается */
.btn:disabled {
  background: var(--muted-bg);
  color: var(--muted-text);
  border-color: transparent;
  cursor: not-allowed;
}

/* Loading: кнопка остаётся в фокусе, повторные нажатия игнорирует скрипт */
.btn[aria-busy="true"] { cursor: progress; }
.btn[aria-busy="true"]::before {
  content: "";
  width: 16px;
  height: 16px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: btn-spin 700ms linear infinite;
}

@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
  .btn[aria-busy="true"]::before { animation-duration: 2s; }
}
Варианты и состояния кнопки на чистом CSS

Атрибут aria-busy="true" сообщает вспомогательным технологиям, что содержимое элемента обновляется; здесь он же служит хуком для стиля спиннера. Сам он ничего не объявляет: результат зачитывает область role="status" — её текст экранные дикторы озвучивают, не перенося фокус.

  1. Hover и active

    Наведите курсор на «Записаться» — фон темнеет; зажмите кнопку мыши — темнеет ещё сильнее, кнопка чуть сдвигается вниз.

  2. Фокус

    Кликните по «Позвонить» — контура нет. Нажмите Tab — контур появляется на «Отмена».

  3. Disabled

    Продолжайте нажимать Tab: кнопка «Нет свободных окон» пропускается, клик по ней ничего не делает.

  4. Loading

    Нажмите Enter на «Записаться» несколько раз подряд: появляется спиннер, отправка одна, фокус остаётся на кнопке, под кнопками появляется статус.

  5. Уменьшение движения

    Включите в системе «уменьшить движение» — плавные переходы отключатся, спиннер замедлится.

Мы прогнали эти шаги автоматически в Microsoft Edge 155 через Playwright: все состояния переключаются, контраст подписи к фону — не ниже 4,5:1 во всех состояниях, включая disabled, контур фокуса к белому фону — 6,7:1.

Поле ввода: label, placeholder, подсказка, ошибка

Поле ввода состоит из подписи (label), самого поля, подсказки под ним и места для сообщения об ошибке. Самая частая ошибка в макетах — убрать подпись и оставить только серый текст внутри поля, placeholder. Nielsen Norman Group разбирает, почему это вредит:

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

Вывод Nielsen Norman Group: подпись и важную подсказку лучше размещать вне поля. Placeholder допустим как пример формата («+7 700 000 00 00»), но не вместо подписи.

  1. Default и focus

    Подпись над полем, рамка контрастна к фону. В фокусе рамка утолщается или меняет цвет, курсор мигает внутри.

  2. Filled

    Введённое значение выглядит иначе, чем placeholder: тёмный текст, а не серый.

  3. Error

    Показывается после попытки отправить форму или после ухода с поля, а не при загрузке страницы. Рамка становится красной, под полем — текст: что не так и как исправить. MDN рекомендует ставить aria-invalid="true" только по результату проверки и связывать поле с текстом ошибки.

  4. Disabled и read-only

    Недоступное поле приглушено. Поле «только для чтения» — другое состояние: значение можно выделить и скопировать.

Карточка: кликабельная область и вложенные действия

Nielsen Norman Group определяет карточку как контейнер для нескольких коротких связанных фрагментов информации — краткое представление объекта со ссылкой на подробности. Типичные части: изображение, заголовок, короткое описание, мета (дата, цена, теги) и иногда второстепенное действие.

Чаще всего вся карточка ведёт на страницу объекта: большая область нажатия удобнее на телефоне и для мыши. Но внутри бывают свои кнопки — «В избранное», «Поделиться». Обернуть всю карточку в ссылку <a> и положить внутрь кнопку нельзя: HTML запрещает вкладывать интерактивные элементы в ссылку. Рабочий приём — ссылкой делают заголовок, а её область нажатия растягивают на всю карточку через CSS; второстепенные кнопки ставят поверх этого слоя.

Как ломается

  • Вся карточка — ссылка, внутри кнопка «В избранное»: невалидная разметка, клик по сердечку открывает страницу
  • Кликабельна только маленькая надпись «Подробнее»
  • Фокус с клавиатуры виден только на тексте, а не на карточке
  • Наведение — единственный признак, что карточка кликабельна

Как стоит

  • Заголовок — ссылка, её область растянута на карточку
  • Второстепенные действия — отдельные кнопки, визуально отделённые от контента
  • При focus-visible контур обводит всю карточку
  • Карточку выдают тень, рамка и заголовок-ссылка, а не только hover

У карточки те же состояния, что у кнопки, плюс свои: hover (тень или подъём), focus-visible, selected — если карточки выбирают, как тарифы, — и loading, когда вместо неё временно стоит skeleton. Nielsen Norman Group напоминает и об ограничении: карточки хороши для просмотра разнородного контента, но плохо подходят, когда варианты нужно сравнивать между собой, — там удобнее список или таблица.

Спецификация компонента для передачи в разработку

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

  • Назначение: какую задачу решает компонент и когда его не использовать
  • Анатомия: схема с подписанными частями и обязательными / необязательными элементами
  • Варианты и размеры: список с примерами применения
  • Матрица «вариант × состояние»: каждая ячейка нарисована или явно помечена «не бывает»
  • Размеры и отступы: высота, внутренние отступы, радиус, минимальная область нажатия — через токены, а не «на глаз»
  • Поведение: что происходит при нажатии, при долгой загрузке, при ошибке; что с фокусом после действия
  • Тексты: правила подписей, максимальная длина, перенос длинного текста
  • Доступность: контраст, видимый фокус, подпись для экранного диктора у кнопки-иконки
Как проверено3 · пройденосверено 11 октября 2026 г.
  • запущено, работает

    index.html + button.css (варианты, hover/active/focus-visible/disabled/aria-busy, role=status)

    статическая страница во временной папке, Playwright 1.64 (из node_modules репозитория) + Microsoft Edge 155 headless: 21 проверка (computed styles по состояниям, :focus-visible после клика мышью/Tab, disabled вне Tab-порядка и без click, одна отправка при повторных нажатиях, фокус сохраняется, статус, reduced motion, контраст ≥4,5:1 / контур 6,7:1), скриншоты 01–06; EN-вариант (verify/…/en) прогнан тем же сценарием — 21 PASS

  • запущено, работает

    danger.css (решение упражнения destructive)

    подключён поверх button.css в Edge 155: hover/active/focus-visible переключаются, контраст белого текста 6,47/8,31/10,02:1

  • запущено, работает

    утверждение «disabled на сфокусированной кнопке сбрасывает фокус»

    Edge 155: после btn.disabled = true document.activeElement = BODY

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.