Клавиатурная доступность означает, что пользователь может выполнить любое действие в интерфейсе — открыть меню, выбрать пункт, отправить форму, закрыть модальное окно — не касаясь мыши или тачскрина. Для Vue и Angular это не отдельная библиотека, а набор практик при написании компонентов: управление фокусом, корректный порядок перехода по Tab, обработка клавиш и правильные роли элементов. В этой статье разберём, что именно нужно сделать, почему стандартные подходы часто ломают навигацию с клавиатуры и как проверить результат.
Главный ориентир простой: если вы не можете полностью пройти свой интерфейс, держа в руках только клавиатуру, — он недоступен. Всё остальное — детали реализации этого принципа.
- Зачем это нужно помимо этики
- Базовые правила, общие для любого фреймворка
- Используйте нативные элементы там, где они подходят
- Не ломайте естественный порядок Tab
- Фокус всегда должен быть виден
- Ловушки фокуса допустимы только в модальных окнах
- Реализация в Vue
- Доступные кастомные компоненты
- Управление фокусом программно
- Динамические списки и уведомления
- Реализация в Angular
- Angular CDK: готовые строительные блоки
- Шаблоны и привязка событий
- Реактивные формы
- Сравнение подходов
- Типичные ошибки и их последствия
- Пошаговый план проверки компонента
- Сценарии: с чего начать в вашем случае
- Частые вопросы
- Достаточно ли добавить tabindex=»0″ ко всему?
- Нужны ли ARIA-атрибуты, если я использую нативные элементы?
- Автоматические проверки заменяют ручное тестирование?
- Что делать с бесконечными лентами и виртуальными списками?
- С чего начать прямо сейчас
Зачем это нужно помимо этики
Поддержка клавиатуры важна не только людям с ограниченными возможностями. Ею активно пользуются:
- продвинутые пользователи, которым быстрее нажать сочетание клавиш, чем вести курсор к кнопке;
- люди с временными ограничениями — травмой руки, усталостью, неудобной позой;
- пользователи программ чтения с экрана (screen readers), которые перемещаются по странице именно через фокус и структуру документа;
- те, кто работает на устройствах без полноценного указателя ввода.
Кроме того, во многих странах требования цифровой доступности закреплены законодательно для государственных сервисов и крупных компаний. Конкретные нормы зависят от юрисдикции и даты, поэтому актуальные обязательства стоит уточнять отдельно — но база везде одна: полный обход интерфейса с клавиатуры.
Базовые правила, общие для любого фреймворка
Прежде чем говорить о Vue и Angular, зафиксируем принципы, которые одинаковы везде, потому что работают на уровне браузера и DOM.
Используйте нативные элементы там, где они подходят
Кнопка должна быть , ссылка — . Нативные элементы бесплатно получают фокус, реакцию на Enter и Space, роль и семантику. Самая частая ошибка — «кнопка» из
Не ломайте естественный порядок Tab
Порядок перехода по Tab должен совпадать с визуальным и логическим порядком чтения: слева направо, сверху вниз. Положительный tabindex (tabindex=»1″ и выше) вырывает элементы из потока и почти всегда приводит к хаотичной навигации. Допустимые значения — отсутствие атрибута, tabindex=»0″ (включить элемент в обход) и tabindex=»-1″ (сделать фокусируемым программно, но исключить из Tab-обхода).
Фокус всегда должен быть виден
Удаление обводки фокуса через outline: none без замены — прямой путь к тому, что пользователь «потеряется» на странице. Если стандартная рамка не вписывается в дизайн, задайте собственный стиль состояния :focus-visible — контрастный, заметный, согласованный с брендом.
Ловушки фокуса допустимы только в модальных окнах
Когда открыто модальное окно, фокус должен оставаться внутри него: Tab зацикливается между первым и последним интерактивным элементом, Escape закрывает окно, а после закрытия фокус возвращается на элемент, который его вызвал. За пределами модальных окон ловушка фокуса — дефект.
Реализация в Vue
Vuе даёт гибкость, но и ответственность: шаблон компонента вы контролируете полностью, поэтому все решения о разметке принимаете сами.
Доступные кастомные компоненты
Типовая задача — выпадающий список (select). Нативный уже доступен, но если дизайн требует кастомного, придётся воспроизвести поведение самостоятельно:
- триггер — настоящий с атрибутами aria-expanded и aria-haspopup;
- список — элемент с role=»listbox», пункты — с role=»option»;
- стрелки вверх и вниз перемещают активный пункт, Enter выбирает, Escape закрывает;
- при открытии фокус переходит в список (или остаётся на триггере с паттерном aria-activedescendant);
- после выбора или закрытия фокус возвращается на триггер.
Это реализация известного паттерна ARIA Authoring Practices — перед написанием собственного сложного виджета полезно свериться с ним: там описаны ожидаемые клавиши для меню, табов, диалогов, аккордеонов и других компонентов.
Управление фокусом программно
Во Vue 3 фокус ставят через ref и хук жизненного цикла:
- после монтирования модального окна — перенести фокус на первый интерактивный элемент или заголовок окна с tabindex=»-1″;
- после закрытия — вернуть фокус на кнопку, которая открыла окно; ссылку на неё удобно сохранить при открытии;
- для циклического обхода внутри диалога — обработчик keydown на контейнере, который при Tab на последнем элементе переносит фокус на первый и наоборот.
Важно дождаться фактического рендера: если ставить фокус сразу после изменения reactive-состояния, элемента ещё нет в DOM. Используйте nextTick.
Динамические списки и уведомления
Если содержимое появляется асинхронно (результаты поиска, тосты), пользователь с клавиатуры должен узнать об этом. Для критичных сообщений используйте живые регионы (aria-live), а новые интерактивные блоки либо делайте следующими в Tab-порядке естественно, либо явно переносите на них фокус — например, на кнопку «Показать ещё результаты».
Реализация в Angular
Angular исторически сильнее ориентирован на доступность: в экосистеме есть Angular CDK с готовыми примитивами, а шаблоны поощряют семантичную разметку.
Angular CDK: готовые строительные блоки
Component Dev Kit содержит пакеты, которые закрывают самые трудоёмкие задачи:
- FocusTrap — удержание фокуса внутри модального окна и автопереход фокуса при открытии;
- FocusMonitor — отслеживание способа появления фокуса (мышь, клавиатура, программа), чтобы показывать обводку только при навигации с клавиатуры;
- AriaDescriber — программное подключение описаний для скринридеров;
- KeyManager — унифицированная обработка стрелок, Home/End и Enter для списков, меню и табов.
Эти же механизмы лежат в основе Angular Material, поэтому изучить их поведение можно прямо на его компонентах: диалоги, меню и селекты там уже решают проблему фокуса за вас.
Шаблоны и привязка событий
Для обработки клавиш Angular предлагает декоративные хост-привязки: host-слушатели на keydown.enter, keydown.escape и подобные позволяют описать реакцию декларативно, рядом с компонентом. Это чище, чем ручное добавление и удаление слушателей, и упрощает тестирование.
Отдельное внимание — директивам ngIf и структурным изменениям: когда блок появляется или исчезает, Tab-порядок меняется автоматически, но фокус никуда не переносится. После условного рендеринга важного блока (ошибка формы, диалог подтверждения) фокус стоит поставить явно, например через ссылку на элемент в ngAfterViewInit.
Реактивные формы
Ошибки валидации должны быть доступны не только визуально. Свяжите поле с сообщением об ошибке через aria-describedby, помечайте проблемные поля aria-invalid и при неудачной отправке переносите фокус на первое некорректное поле — иначе пользователь с клавиатуры или скринридером просто не поймёт, что пошло не так.
Сравнение подходов
| Аспект | Vue | Angular |
|---|---|---|
| Готовые примитивы доступности | Нет встроенных; используются сторонние библиотеки или собственная реализация | Angular CDK входит в официальную экосистему |
| Обработка клавиш | Слушатели в шаблоне или на window/document в composables | Хост-привязки keydown.* и KeyManager из CDK |
| Управление фокусом | Ручное: ref + nextTick, свои ловушки фокуса | FocusTrap и FocusMonitor из коробки |
| Свобода разметки | Полная, всё пишете сами | Высокая, но есть готовые компоненты Material |
| Порог входа | Нужно знать паттерны ARIA самостоятельно | Ниже, если использовать CDK/Material |
Выбор здесь не про «какой фреймворк лучше», а про объём работы: в Angular значительная часть инфраструктуры уже есть, во Vue её собираете сами или берёте из библиотек вроде Headless UI, Radix Vue и аналогичных headless-решений, которые дают логику и доступность без навязывания стилей.
Типичные ошибки и их последствия
- Div вместо button. Элемент не попадает в Tab-обход, не реагирует на Enter и Space, скринридер объявляет его как обычный текст. Последствие — действие недоступно без мыши.
- outline: none без замены. Пользователь не видит, где находится фокус. Это одна из самых разрушительных ошибок: интерфейс формально проходим, но фактически непроходим.
- Положительный tabindex. Порядок обхода перестаёт совпадать с визуальным, часть элементов пропускается или посещается дважды.
- Модальное окно без управления фокусом. Tab уводит фокус под оверлей на скрытые элементы страницы, Escape не работает, после закрытия фокус теряется в начале документа.
- Скрытие элементов через display:none при этом оставляя их «фокусируемыми». Обратная ситуация: элемент визуально скрыт, но доступен с клавиатуры, и фокус «исчезает» в пустоту. Скрывайте интерактивные элементы так, чтобы они выпадали и из Tab-обхода.
- Обработка только click. Событие click на нативной кнопке срабатывает и от Enter, и от Space, но на кастомных элементах — нет. Всегда добавляйте keydown-обработчики для Enter и Space на нестандартных интерактивных узлах.
- Горячие клавиши без учёта режима ввода. Одиночные буквенные сочетания конфликтуют с вводом текста и скринридерами. Привязывайте глобальные хоткеи к модификаторам или давайте возможность их отключить.
Пошаговый план проверки компонента
- Отключите мышь физически или работайте строго с клавиатурой: пройдите весь сценарий от загрузки до целевого действия.
- Проверьте порядок Tab: он должен идти по визуальному порядку без пропусков и скачков.
- Убедитесь, что фокус всегда виден: на каждом шаге вы понимаете, какой элемент активен.
- Проверьте каждую интерактивную зону: кнопки реагируют на Enter и Space, ссылки — на Enter, чекбоксы переключаются пробелом.
- Для сложных виджетов сверьтесь с паттерном ARIA Authoring Practices: какие клавиши ожидаются для меню, табов, списка — те и должны работать.
- Протестируйте модальные окна: фокус входит внутрь, зациклен, Escape закрывает, после закрытия фокус возвращается на вызывающий элемент.
- Проверьте формы: ошибки озвучиваются и связаны с полями, фокус после неудачной отправки попадает на проблемное место.
- При желании прогоните страницу скринридером или автоматическим аудитом (например, Lighthouse), но помните: автоматика ловит лишь часть проблем, ручной проход с клавиатурой незаменим.
Сценарии: с чего начать в вашем случае
- Проект только начинается. Сразу договоритесь о правиле «нативные элементы по умолчанию» и подключите headless-библиотеку со встроенной доступностью для сложных виджетов — это дешевле, чем переписывать позже.
- Готовый продукт без поддержки клавиатуры. Начните с аудита ключевых сценариев (регистрация, покупка, основное действие) и исправьте критичное: div-кнопки, невидимый фокус, модалки. Это даст максимальный эффект при минимальных вложениях.
- Дизайн-система. Решения о фокусе, клавишах и ролях принимайте один раз на уровне базовых компонентов, а не в каждом экране. Иначе каждый разработчик будет изобретать своё поведение.
- Сложный виджет (редактор, таблица, календарь). Не пишите с нуля без необходимости: возьмите проверенную реализацию или тщательно сверьтесь с паттернами ARIA — такие компоненты легко сделать формально работающими, но непроходимыми с клавиатуры.
Частые вопросы
Достаточно ли добавить tabindex=»0″ ко всему?
Нет. Это сделает обход длинным и бессмысленным: фокус будет останавливаться на декоративных и неинтерактивных элементах. Фокусируемыми должны быть только элементы, с которыми можно взаимодействовать, плюс точки программного фокуса с tabindex=»-1″.
Нужны ли ARIA-атрибуты, если я использую нативные элементы?
В большинстве случаев нет: нативная кнопка уже имеет роль. ARIA нужен для кастомных виджетов, живых регионов, связей «поле — ошибка» и состояний вроде aria-expanded. Избыточные роли иногда даже вредят, потому что перекрывают семантику.
Автоматические проверки заменяют ручное тестирование?
Нет. Инструменты находят технические нарушения (отсутствие alt, контраст, дублирующиеся id), но не могут пройти сценарий и оценить логику порядка фокуса или поведение модального окна. Комбинируйте оба подхода.
Что делать с бесконечными лентами и виртуальными списками?
Следите, чтобы при подгрузке новых элементов фокус не сбрасывался и не «прыгал», а кнопка загрузки была достижима с клавиатуры. В виртуализированных списках убедитесь, что прокрутка фокусом работает и активный элемент реально существует в DOM в момент получения фокуса.
С чего начать прямо сейчас
Возьмите один ключевой сценарий вашего приложения и пройдите его только с клавиатурой, записывая каждое место, где вы запнулись. Этот список и есть ваш план работ, отсортированный по реальной важности. Дальше двигайтесь по нему: сначала восстановите видимый фокус и нативные элементы, затем приведите в порядок модальные окна и формы, и только потом усложняйте — горячие клавиши, кастомные виджеты, живые регионы. Клавиатурная доступность — это не разовая задача, а правило разработки, которое дешевле соблюдать с первого дня, чем внедрять постфактум.
