Состояния UI-компонентов: как проектировать hover, focus, disabled и error без дыр в интерфейсе

Состояния UI-компонентов: как проектировать hover, focus, disabled и error без дыр в интерфейсе

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

Большинство таких дыр появляются не из-за плохого вкуса, а из-за системного пропуска. Дизайнер прорабатывает дефолтный вид и забывает про остальные шесть-восемь вариантов поведения элемента. Разработчик реализует то, что видит на макете. В итоге продукт работает, но не до конца.

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

Коротко:

  • Каждый интерактивный элемент имеет минимум 6-8 состояний: default, hover, focus, active, disabled, error, loading, success/selected.
  • Пропуск даже одного из них создает визуальную или функциональную дыру - особенно заметную на клавиатурной навигации и в граничных сценариях.
  • Hover сигнализирует о кликабельности, focus - о текущей позиции при навигации с клавиатуры, disabled - о недоступности без объяснения причины.
  • Ошибка в error-состоянии - это коммуникация, а не просто красная рамка: важно где, что и почему.
  • В Figma каждое состояние должно быть отдельным вариантом компонента, а не отдельным фреймом.
  • Перед хендофом проверяй компонент по чеклисту - это экономит время на правках после разработки.

Почему состояния ломаются чаще, чем кажется

Дизайнер работает в статичном инструменте. Figma показывает один момент времени, и соблазн нарисовать «красивый экран» сильнее, чем желание перебрать все сценарии взаимодействия. Джуны и мидлы часто сдают компонент с двумя вариантами: default и hover. Остальное «очевидно».

На практике очевидного не существует. Разработчик реализует ровно то, что видит. Если focus не нарисован - его не будет. Если disabled выглядит как default с opacity 0.5 - именно так и будет выглядеть в продукте, даже если это нечитаемо на темном фоне.

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

Полная карта состояний: что должно быть у каждого компонента

Набор состояний зависит от типа элемента, но базовый список выглядит так:

СостояниеКогда активноЧто должно измениться визуально
DefaultЭлемент доступен, курсор не на нёмБазовый вид без изменений
HoverКурсор наведёнЦвет фона, обводка, тень, курсор pointer
FocusЭлемент выбран с клавиатуры или тапомВидимый контур (outline), никогда не убирать
Active / PressedМомент нажатияСдвиг, затемнение, scale вниз
DisabledДействие недоступноПониженная непрозрачность, cursor not-allowed
ErrorВалидация не прошлаКрасная рамка, иконка, текст ошибки
LoadingОжидание ответа сервераСпиннер или skeleton, блокировка повторного клика
Success / SelectedДействие выполнено или элемент выбранЗелёный акцент, галочка, изменение лейбла

Не каждый компонент требует всех восьми. У статичного текстового поля не будет active в том же смысле, что у кнопки. У тоггла не будет error в базовом сценарии. Но прежде чем исключить состояние, стоит явно ответить: «это точно не нужно здесь» - а не просто забыть о нём.

Hover: сигнал о кликабельности

Наведение курсора - первый момент, когда пользователь понимает, можно ли с элементом взаимодействовать. Если ничего не меняется, человек не уверен: это кнопка или просто блок?

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

Типичные способы обозначить наведение:

  • Изменение фона кнопки на 10-15% темнее или светлее
  • Появление тени (box-shadow) у карточки
  • Подчёркивание у ссылки
  • Смена иконки или её цвета
  • Лёгкое масштабирование (scale 1.02) для карточек

Главная ошибка - делать hover только для кнопок и игнорировать карточки, иконки, строки таблицы и другие кликабельные блоки. Если элемент реагирует на клик, он должен реагировать и на наведение.

Антипаттерн: убирать hover на мобильных устройствах «потому что там нет мыши» и забывать добавить его обратно для десктопной версии. Hover - это десктопный паттерн, но в адаптивном дизайне важно явно разделять поведение для touch и pointer устройств через CSS-медиазапросы @media (hover: hover).

Focus: состояние, которое чаще всего убивают

Focus state - это индикатор текущей позиции при навигации с клавиатуры. Он нужен не только людям с нарушениями моторики, но и всем, кто пользуется Tab, стрелками или скринридером.

Самая распространённая ошибка в focus state интерфейс - это outline: none в CSS. Разработчик убирает стандартный синий контур браузера, потому что он «некрасивый», и не заменяет его ничем. В результате пользователь с клавиатуры теряет ориентацию: непонятно, на каком элементе он находится.

Правила для focus:

  • Контур должен быть виден на любом фоне - используй достаточный контраст (минимум 3:1 по WCAG 2.1)
  • Толщина outline - минимум 2px, лучше 3px
  • Не используй только цвет как единственный индикатор: добавь форму или тень
  • Focus-ring должен отличаться от hover - это разные состояния с разным смыслом
  • Для кастомных компонентов (дропдаун, модалка, слайдер) прописывай focus явно - браузер не сделает это автоматически

В Figma удобно использовать отдельный вариант компонента с именем «focus» и явным outline. Это помогает разработчику понять, что состояние существует и как оно должно выглядеть.

Active и Pressed: момент нажатия

Это самое короткое по времени состояние - оно длится пока кнопка зажата. Но без него интерфейс кажется «мёртвым»: пользователь нажимает и не понимает, сработало ли.

Для кнопок active обычно означает:

  • Небольшой сдвиг вниз (translateY 1-2px) - имитация физического нажатия
  • Более тёмный фон по сравнению с hover
  • Уменьшение тени или её исчезновение

На мобильных устройствах это состояние особенно важно, потому что у пользователя нет курсора и hover недоступен. Тактильный отклик через визуальное изменение - единственный способ подтвердить касание.

Disabled: недоступность с объяснением

Disabled состояние UX - одна из самых недооценённых тем. Дизайнеры часто ставят opacity 0.5 и считают задачу выполненной. Но у заблокированного элемента есть несколько проблем, которые нужно решить заранее.

Проблема 1: пользователь не понимает, почему элемент недоступен. Кнопка «Отправить» серая - почему? Не заполнено поле? Нет прав? Нужно подождать? Без подсказки человек будет кликать снова и снова или уйдёт.

Проблема 2: disabled элемент может быть нечитаемым. Текст на кнопке с opacity 0.5 на светлом фоне теряет контраст. По WCAG декоративные неактивные элементы освобождены от требований контраста, но если на кнопке есть текст с информацией - пользователь должен его прочитать.

Проблема 3: disabled и read-only - разные вещи. Read-only поле показывает значение, но не даёт его изменить. Disabled поле вообще исключается из формы и не отправляется с данными. Визуально они могут выглядеть похоже, но семантически и функционально - нет.

Пример: кнопка «Продолжить» в форме регистрации заблокирована, пока не заполнены обязательные поля. Вместо того чтобы просто делать её серой, добавь тултип при наведении: «Заполните email и пароль». Это снижает фрустрацию и объясняет логику.

Альтернативный подход - не делать кнопку disabled вообще, а показывать ошибки валидации при попытке отправить незаполненную форму. Это спорный момент в UX-сообществе, но он работает лучше в сценариях, где пользователь не понимает, что именно нужно исправить.

Error: красная рамка - это не решение

Ошибка в интерфейсе - это момент коммуникации. Пользователь сделал что-то не так или система не смогла выполнить действие. Задача дизайнера - объяснить что случилось, где проблема и как её исправить.

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

Правила для error-состояния:

  • Всегда добавляй текст ошибки под полем - конкретный, не «Ошибка ввода»
  • Используй иконку вместе с цветом - не полагайся только на красный (дальтоники)
  • Сохраняй введённое значение - не очищай поле при ошибке
  • Показывай ошибку рядом с полем, а не только в шапке формы
  • Для серверных ошибок (недоступность API, конфликт данных) используй отдельный паттерн - не тот же, что для валидации

Отдельный вопрос - когда показывать ошибку. Три подхода: при потере фокуса (blur), при отправке формы, или в реальном времени (live validation). Каждый подходит для разных сценариев. Подробнее об этом - в статье про валидацию форм, которая уже есть в блоге.

Loading: блокировка и обратная связь

Когда пользователь нажал кнопку и ждёт ответа сервера, интерфейс должен показать два вещи: действие выполняется, и повторный клик заблокирован.

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

Для проектирования состояний кнопок в loading-режиме:

  • Замени текст на спиннер или добавь спиннер рядом с текстом
  • Заблокируй кнопку (disabled) на время запроса
  • Если загрузка долгая (больше 3 секунд) - добавь прогресс или текст «Обрабатываем...»
  • После завершения явно переходи в success или error - не возвращайся молча в default

Selected и Success: подтверждение действия

Selected - это состояние выбранного элемента в группе: таб, чекбокс, радиокнопка, фильтр. Success - подтверждение завершённого действия (форма отправлена, файл загружен, настройка сохранена).

Частая ошибка - не различать selected и hover визуально. Пользователь наводит курсор на уже выбранный таб и не понимает, выбран он или нет, потому что оба состояния выглядят одинаково.

Для success важно решить, как долго показывать подтверждение. Если кнопка «Сохранить» на секунду показывает галочку и возвращается в default - это нормально. Если форма исчезает и появляется пустой экран без объяснений - пользователь не уверен, что данные сохранились.

Как организовать состояния в Figma

UI states Figma - это не просто набор фреймов с разными видами кнопки. Правильная организация экономит время при обновлениях и делает хендоф понятным.

Базовые принципы:

  • Каждое состояние - отдельный вариант (variant) внутри компонента, а не копия в другом месте файла
  • Называй варианты консистентно: Default, Hover, Focus, Active, Disabled, Error, Loading, Success
  • Используй component properties для переключения состояний - это позволяет разработчику видеть все варианты в одном месте
  • Для интерактивных компонентов (кнопки, поля) настраивай Interactive Components с переходами между состояниями
  • Документируй в аннотациях, какое состояние когда активируется - особенно для нестандартных случаев

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

Типичные ошибки при проработке состояний

ОшибкаПоследствиеКак исправить
Убрать outline у focusКлавиатурная навигация невозможнаЗаменить на кастомный focus-ring с контрастом 3:1+
Disabled без объяснения причиныПользователь не знает, что делатьДобавить тултип или подсказку рядом с элементом
Error только через цветДальтоники не видят ошибкуДобавить иконку и текст ошибки
Кнопка активна во время загрузкиДублирование запросовБлокировать кнопку и показывать спиннер
Hover только у кнопокКликабельные карточки выглядят статичноДобавить hover для всех интерактивных элементов
Selected и hover выглядят одинаковоПользователь не понимает текущий выборРазделить визуально: разные цвета или веса

Чеклист перед хендофом

Прежде чем передавать компонент в разработку, пройдись по этому списку:

  1. Все восемь состояний нарисованы или явно исключены с обоснованием
  2. Focus-ring виден на всех фонах, контраст минимум 3:1
  3. Disabled элементы имеют подсказку о причине недоступности
  4. Error-состояние включает текст ошибки, иконку и не только цвет
  5. Loading блокирует повторный клик и показывает индикатор
  6. Hover добавлен для всех кликабельных элементов, не только кнопок
  7. Selected визуально отличается от hover
  8. Все варианты организованы как variants в Figma, а не разрозненные фреймы
  9. Цвета состояний привязаны к токенам или переменным
  10. Аннотации объясняют нестандартное поведение (например, когда показывать error - при blur или submit)

FAQ

Нужно ли рисовать все состояния для каждого компонента?

Не обязательно все восемь для каждого. Статичный текст не имеет hover. Радиокнопка не имеет loading. Но прежде чем пропустить состояние, задай себе вопрос: «Что произойдёт в этом сценарии?» Если ответ «ничего» - это осознанное решение. Если «не знаю» - нужно разобраться.

Как сделать focus видимым, не нарушая дизайн?

Кастомный focus-ring не обязан быть синим браузерным контуром. Можно использовать цвет акцента продукта, тень или offset-outline. Главное - контраст минимум 3:1 и толщина от 2px. Хорошо спроектированный focus органично вписывается в систему и не выглядит чужеродно.

Чем отличается disabled от read-only?

Read-only поле показывает значение, которое нельзя редактировать, но оно участвует в форме и отправляется с данными. Disabled полностью исключает элемент из взаимодействия и из отправки формы. Визуально они могут быть похожи, но семантически - разные. Это важно объяснить разработчику в аннотациях.

Когда лучше не делать кнопку disabled, а показывать ошибки при отправке?

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

Как организовать интерактивные состояния компонентов в Figma для большой библиотеки?

Используй variants с явными именами свойств (State: Default / Hover / Focus / Disabled и т.д.). Для сложных компонентов с несколькими осями изменений (размер + состояние + тип) применяй component properties. Это делает библиотеку предсказуемой для всей команды и упрощает хендоф.

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

На touch-устройствах hover в классическом смысле недоступен. Но браузеры иногда эмулируют его при тапе, что может создавать залипающие состояния. Используй медиазапрос @media (hover: hover), чтобы применять hover-стили только там, где есть указательное устройство. На мобильных акцент переносится на active/pressed.

Как проверить, что все состояния реализованы правильно в продукте?

Пройдись по компоненту вручную: наведи курсор, нажми Tab, кликни, введи невалидное значение, отправь форму с пустыми полями, дождись ошибки сервера. Для клавиатурной навигации используй только Tab и Enter без мыши. Если в каком-то сценарии интерфейс не реагирует или реагирует неожиданно - это баг, который нужно зафиксировать.

Итог

Проработка всех состояний - это не перфекционизм и не лишняя работа. Это базовый уровень качества компонента. Пропущенный focus ломает доступность. Непонятный disabled фрустрирует пользователя. Кнопка без loading создаёт дубли в базе данных.

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

Хорошо спроектированный компонент - это тот, который ведёт себя предсказуемо в любом сценарии. Пользователь не замечает состояний, когда они сделаны правильно. Он замечает только тогда, когда что-то идёт не так.