Состояния 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)

часто задаваемые вопросы

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

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

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

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

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

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

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

Итог

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

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

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