Состояния компонента: hover, focus, disabled, loading, error, empty

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

Редакция Флагштока17 сентября 20268 мин чтения
Состояния компонента: hover, focus, disabled, loading, error, empty

Почему состояния — часть компонента

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

Состояния стоит описывать как полноценную часть спецификации: визуал, поведение, текст, разметку и требования доступности. В Figma их удобно делать вариантами компонента, а в коде — проверять в Storybook или аналогичном каталоге, где каждое состояние имеет отдельную историю.

Важно различать два типа состояний. Интерактивные — hover, pressed, focus — возникают от действий пользователя и длятся мгновения. Состояния данных — loading, error, empty, disabled — зависят от системы и могут держаться долго. Первые влияют на ощущение отзывчивости, вторые — на понимание того, что происходит.

Если состояние не нарисовано и не описано, его всё равно кто-то сделает — но без системы.

Hover и pressed: отклик на указатель

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

На сенсорных экранах hover не существует, а эмуляция браузером может оставить элемент «залипшим» после касания. Поэтому стили наведения лучше оборачивать в медиазапрос @media (hover: hover) и никогда не прятать за hover единственный способ получить информацию или действие.

Pressed, или active, показывает момент нажатия. Обычно это более тёмный оттенок или лёгкое смещение. Это состояние особенно важно на мобильных, где оно — единственный мгновенный отклик на касание до того, как система успеет отреагировать.

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

Иллюстрация к разделу «Hover и pressed: отклик на указатель»

Focus: самое недооценённое состояние

Фокус показывает, какой элемент получит ввод с клавиатуры. Для людей, которые не пользуются мышью, это единственный ориентир в интерфейсе. Критерий 2.4.7 WCAG требует, чтобы индикатор фокуса был виден, а в версии 2.2 добавлен критерий 2.4.11: элемент в фокусе не должен полностью закрываться липкими шапками, баннерами и другими слоями.

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

Индикатор фокуса должен быть контрастным. По критерию 1.4.11 визуальные индикаторы состояний требуют контраста не ниже 3:1 с соседними цветами. Надёжный приём — контур толщиной 2 пикселя с отступом от элемента (outline-offset): он виден и на светлом, и на цветном фоне.

Проектируйте фокус для всех интерактивных элементов, включая карточки-ссылки, вкладки, пункты меню и элементы внутри модальных окон. Порядок перехода должен совпадать с визуальным порядком, а при открытии диалога фокус переносится внутрь и возвращается назад после закрытия.

  • видим при навигации с клавиатуры
  • контраст не ниже 3:1
  • не перекрыт липкими слоями
  • логичный порядок обхода

Disabled: недоступно, но понятно почему

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

Главная проблема disabled — молчание. Пользователь видит серую кнопку «Отправить» и не понимает, что не так. Если действие недоступно из-за незаполненных полей, лучше оставить кнопку активной и показать ошибки при нажатии, либо явно объяснить рядом, чего не хватает.

Технически есть два подхода. Атрибут disabled убирает элемент из порядка табуляции и скрывает от взаимодействия. Атрибут aria-disabled="true" оставляет элемент фокусируемым, и пользователь скринридера может узнать о нём и прочитать пояснение. Второй вариант требует блокировать действие в обработчике, но лучше подходит для кнопок, которые важно найти.

Отключённая кнопка без объяснения — это вопрос без ответа. Скажите пользователю, что нужно сделать.

Loading: ожидание, которое не пугает

Классические пороги времени отклика, которые описывал ещё Роберт Миллер в 1968 году и позже популяризировал Нильсен, по-прежнему полезны: около 0,1 секунды ощущается как мгновенная реакция, до 1 секунды пользователь не теряет нить, а после 10 секунд внимание уходит. Отсюда правило: короткие операции не требуют индикатора, средние — требуют, длинные — нуждаются в прогрессе и возможности отменить.

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

Не забывайте о скринридерах. Область, которая обновляется, можно пометить aria-busy="true", а сообщение о завершении вывести в регион с role="status". Критерий 4.1.3 WCAG требует, чтобы такие статусные сообщения были доступны без перемещения фокуса.

Error и empty: когда что-то пошло не так или ничего нет

Ошибка должна отвечать на три вопроса: что случилось, где и как исправить. По критерию 3.3.1 ошибку нужно описать текстом, а не только цветом. Для полей ввода связывайте сообщение с полем через aria-describedby и отмечайте поле aria-invalid="true". Иконка рядом с красной рамкой помогает пользователям, которые не различают красный.

Текст ошибки пишется на языке пользователя. «Введите дату в формате ДД.ММ.ГГГГ» полезнее, чем «Неверный формат». При ошибке отправки формы введённые данные должны сохраняться, а фокус — переходить к первому проблемному полю или к сводке ошибок.

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

  • что случилось
  • где именно
  • как исправить
  • данные пользователя сохранены

Матрица состояний в дизайн-системе

Удобный способ ничего не упустить — матрица: строки — компоненты, столбцы — состояния. Для каждой клетки отмечается, применимо ли состояние, нарисовано ли в Figma, реализовано ли в коде и проверено ли на доступность. Пустые клетки сразу видны на ревью.

Учитывайте комбинации. Поле может быть одновременно в фокусе и с ошибкой, кнопка — в hover и loading. Не обязательно рисовать все сочетания, но нужно определить приоритет: например, ошибка сильнее hover, а focus всегда видим поверх остальных состояний.

Цвета состояний выносите в семантические токены — color.border.focus, color.text.danger, color.surface.disabled. Тогда в тёмной теме и при ребрендинге все состояния обновятся согласованно, а не по одному.

Наконец, включите состояния в критерии готовности задачи. Компонент считается готовым, когда все применимые клетки матрицы закрыты: есть макет, реализация, история в каталоге и пройдена проверка с клавиатурой. Такое правило дисциплинирует сильнее любых договорённостей на словах.

Частые вопросы

Нужно ли делать hover для мобильной версии?

Нет, на сенсорных экранах наведения нет. Вместо него важно состояние pressed — мгновенный отклик на касание. Hover-стили лучше ограничить медиазапросом (hover: hover), чтобы они не залипали после тапа.

Можно ли убрать обводку фокуса, если она не нравится дизайнеру?

Убрать без замены нельзя: это нарушит критерий 2.4.7 WCAG. Можно перерисовать индикатор в стиле продукта через :focus-visible, сохранив заметность и контраст не ниже 3:1.

Что лучше для загрузки списка: скелетон или спиннер?

Для контента со знакомой структурой обычно лучше скелетон: он показывает, что появится, и уменьшает сдвиги вёрстки. Спиннер уместен для коротких действий и в местах, где структура результата заранее неизвестна.

Редакция Флагштока

Пишем об UX/UI-дизайне: исследования, интерфейсы, дизайн-системы и доступность.