В прошлом году я три дня аудировал checkout-поток, у которого был рейтинг 4.8/5 в дизайн-ревью Figma. Визуально - безупречно. Но пользователи, работающие только с клавиатурой, не могли перейти в поле промокода: там был <div> с обработчиком click, без tabindex, без role. Пользователи скринридеров слышали 'без названия' на каждом инпуте, потому что дизайнер использовал placeholder как лейбл, а разработчик скопировал паттерн. Сообщения об ошибках отображались красным, но не были программно связаны с полями. Кнопка 'Оформить заказ' перехватывала Enter, но не Space. Каждый из этих багов был невидим для всех, кто работает мышью.
Это устойчивый паттерн проблем с доступностью: они невидимы для большинства команды и полностью блокируют конкретную группу пользователей. В этом посте я разбираю, как скринридеры реально парсят DOM, что требует WCAG 2.2 в конкретных терминах реализации, какие паттерны клавиатурной навигации ломаются чаще всего и как выстроить тестовый процесс, который находит эти проблемы раньше пользователей.
Часть I - Кто использует ассистивные технологии и почему это важнее, чем просто соответствие требованиям
- Пользователи скринридеров: По данным WHO, 285 миллионов человек в мире имеют нарушения зрения. Доли рынка скринридеров: JAWS (38%), NVDA (31%), VoiceOver (iOS/macOS, 18%), TalkBack (Android, 7%). NVDA бесплатен, VoiceOver встроен в каждое устройство Apple, TalkBack - в Android. Эти пользователи уже есть в вашей аудитории.
- Пользователи только с клавиатурой: Нарушения моторики затрагивают около 2 миллионов американцев, использующих альтернативные устройства ввода - переключатели, управление дыханием, трекеры взгляда. Все они эмулируют клавиатурный ввод на уровне ОС. Если клавиатурная навигация сломана, продукт полностью недоступен для этой группы.
- Когнитивные и учебные нарушения: Дислексия затрагивает 15–20% населения, СДВГ - 8–10% взрослых. Обе группы выигрывают от чёткой типографической иерархии, предсказуемых паттернов навигации, видимых состояний фокуса и возможности управлять контентом на основе времени.
- Правовое воздействие: Закон об американцах с ограниченными возможностями (ADA), EN 301 549 (ЕС) и Equality Act (Великобритания) устанавливают правовые требования к цифровой доступности. Количество ADA-исков против веб-сайтов превысило 4600 в 2023 году - рост 42% год к году (UsableNet).
- Временные и ситуативные ограничения: Сломанная рука, яркое солнце, шумная среда - контраст, управление с клавиатуры и субтитры помогают всем в субоптимальных условиях. Инклюзивный дизайн производит лучшие интерфейсы для всех.
Часть II - Как скринридеры реально парсят DOM
Ментальная модель, которая убирает большинство ARIA-ошибок: скринридеры читают не HTML - они читают дерево доступности. Дерево доступности - это параллельная структура, которую браузер строит из DOM и передаёт через платформенные API. Каждый узел имеет четыре ключевых свойства: role, name, state и value.
- Role: Определяется тегом HTML-элемента (
<button>имеет role 'button') или переопределяется атрибутомrole.<div>без role - это 'generic' контейнер: скринридер прочитает его текст в режиме просмотра, но пользователи не найдут его по быстрым клавишам 'B' (следующая кнопка) или 'F' (следующее поле формы). - Name: Доступное имя - то, что скринридер объявляет при фокусировке. Источники по приоритету:
aria-labelledby,aria-label, содержимое элемента (для кнопок и ссылок), связанный<label>(для инпутов). Placeholder-текст - НЕ источник имени: он исчезает при вводе и ненадёжно объявляется. Это самая распространённая ошибка разметки. - State: Раскрыт/свёрнут (
aria-expanded), отмечен (aria-checked), невалиден (aria-invalid), отключён. Состояние должно обновляться программно при изменении - аккордеон, который визуально открывается, должен менятьaria-expandedна триггере. - Режим просмотра vs интерактивный режим: NVDA/JAWS по умолчанию запускаются в режиме просмотра - стрелки перемещают виртуальный курсор по дереву доступности. При фокусировке текстового инпута переходят в интерактивный режим. Именно поэтому нативная семантика критична -
<div>сonclickне переключает режим, и пользователи не могут с ним взаимодействовать.
Часть III - Клавиатурная навигация: паттерны, которые ломаются чаще всего
- Порядок табуляции и порядок DOM: Порядок табуляции по умолчанию следует порядку источника DOM, а не визуальному макету. CSS flexbox и grid позволяют визуально переставлять элементы без изменения порядка DOM - это создаёт расхождение между визуальным и клавиатурным порядком. Порядок источника DOM должен соответствовать предполагаемому порядку чтения.
- Focus trap в модальных окнах: При открытии модала фокус должен быть заблокирован внутри. Пользователи, нажимающие Tab, должны циклически перемещаться только по фокусируемым элементам модала. При закрытии модала фокус должен вернуться к триггеру. Используйте библиотеку
focus-trapили HTML-элемент<dialog>, который обрабатывает это нативно. - Misuse tabindex:
tabindex='0'- элемент фокусируем в порядке DOM (для кастомных интерактивных элементов).tabindex='-1'- элемент фокусируем только программно, не через Tab (для контейнеров диалогов). Значенияtabindexбольше 0 создают непредсказуемый порядок табуляции - никогда не используйте. - Клавиатурные паттерны для кастомных виджетов: Руководство ARIA APG определяет ожидаемое поведение клавиатуры. Кастомный дропдаун:
Enter/Spaceоткрывает меню, стрелки навигируют по опциям,Enterвыбирает,Escapeзакрывает. Кастомные табы: стрелки переключают вкладки (roving tabindex),Tabпереходит в панель. Реализация половины паттерна хуже, чем отсутствие реализации. - Skip navigation links: Первый фокусируемый элемент на странице - ссылка 'Перейти к основному контенту', пропускающая навигацию. Реализация:
<a href='#main-content' class='sr-only focus:not-sr-only'>Перейти к основному контенту</a>плюс<main id='main-content' tabindex='-1'>.tabindex='-1'позволяет<main>получать программный фокус. - Видимость фокуса: WCAG 2.2 SC 2.4.11 требует, чтобы сфокусированный элемент не был полностью скрыт залипающими заголовками или баннерами. Используйте
scroll-margin-topдля учёта высоты sticky-заголовка.:focus-visibleтаргетирует клавиатурный фокус специфически - применяйте для чётких индикаторов фокуса без влияния на пользователей мыши.
Часть IV - WCAG 2.2: критерии, которые реально встречаются в аудитах
- SC 1.1.1 Нетекстовый контент (Level A): Каждое изображение должно иметь текстовую альтернативу. Информационные изображения:
altописывает содержимое. Декоративные:alt=''(пустая строка, не отсутствующий атрибут). Функциональные (иконки-кнопки):altилиaria-labelописывает действие. SVG-иконки в кнопках:aria-hidden='true'на SVG плюс скрытый текст илиaria-labelна кнопке. - SC 1.4.3 Контрастность (Level AA): Обычный текст - 4.5:1, крупный (18pt / 14pt жирный) - 3:1, UI-компоненты (границы инпутов) - 3:1. Частые ошибки: светло-серый placeholder, белый текст на светлом брендовом цвете.
- SC 2.1.1 Клавиатура (Level A): Весь функционал доступен с клавиатуры. Ошибка: кастомные интерактивные элементы с обработчиками только мыши. Каждый
clickна ненативном элементе нужен соответствующийkeydownдляEnterи частоSpace. Нативная<button>обрабатывает это автоматически. - SC 3.3.1 Идентификация ошибок (Level A) и 3.3.2 Labels (Level A): Ошибки должны быть описаны текстом, а не только цветом. Сообщение об ошибке должно быть программно связано с инпутом через
aria-describedby, инпут должен иметьaria-invalid='true'. - SC 4.1.2 Имя, роль, значение (Level A): Для всех UI-компонентов имя, роль и состояние должны определяться ассистивными технологиями. Каждый интерактивный компонент должен раскрывать свою роль, доступное имя и текущее состояние.
Часть V - Типичные production-ошибки и паттерн исправления
- Кнопки из `<div>` и `<span>`:
<div onClick={handler}>- нет role, нет доступа с клавиатуры, нет фокуса. Используйте<button type='button'>для всех интерактивных элементов, которые не являются навигационными ссылками. - Кнопки-иконки без доступного имени:
<button><Icon /></button>объявляется как 'кнопка' без лейбла. Добавьтеaria-label='Закрыть диалог'на кнопку или скрытый текст<span class='sr-only'>Закрыть</span>. На SVG -aria-hidden='true'. - Поля форм без постоянных лейблов: Использование placeholder как единственного лейбла. Всегда используйте
<label for>илиaria-label. Сообщения об ошибках:<input aria-describedby='email-error' aria-invalid='true' /><p id='email-error'>Введите валидный email</p>. - Информация только через цвет: Состояния ошибок только красным цветом, обязательные поля только красной звёздочкой. Дополняйте цвет текстовым лейблом, паттерном или иконкой с alt.
- Динамический контент без объявления: AJAX-результаты, уведомления, live search - без ARIA live regions скринридер не знает об изменениях. Добавьте
aria-live='polite'на контейнер, получающий динамические обновления. Контейнер должен существовать в DOM до обновления. - Бесконечная прокрутка без выхода с клавиатуры: Пользователи клавиатуры застревают в растущем списке. Решение: пагинация (доступна по умолчанию), кнопка 'Загрузить ещё' в конце батча, или skip-ссылка 'Перейти в конец результатов'.
Часть VI - Тестирование: workflow, который находит реальные проблемы
- Автоматизация: axe-core в CI: Интегрируйте через
@axe-core/playwright. В CI: запускайтеaxeна каждой странице и завершайте сборку приcriticalилиseriousнарушениях. Ловит пропущенные лейблы, нарушения контраста, некорректное использование ARIA. - DevTools: панель Accessibility: Chrome DevTools, вкладка Accessibility показывает дерево доступности любого элемента и его вычисленное имя, роль и состояние. Используйте для отладки, почему скринридер не объявляет ожидаемое имя.
- Тестирование клавиатурой: навигация без мыши: Пройдите Tab по всем интерактивным элементам страницы. Каждый элемент должен быть достижим, виден при фокусировке и управляем через
Enter/Space. Тестируйте модалы, формы, кастомные виджеты. - Тестирование скринридером: VoiceOver и NVDA: На macOS: VoiceOver (
Command+F5) с Safari. Закройте глаза и навигируйте только клавиатурой и слухом. На Windows: NVDA (бесплатный) с Firefox или Chrome. Проверьте: объявляется ли каждый интерактивный элемент с осмысленным именем, объявляются ли изменения состояния, читаются ли сообщения об ошибках.
Часть VII - Доступные паттерны компонентов
- Модальные диалоги:
role='dialog',aria-modal='true',aria-labelledbyуказывает на заголовок диалога. Фокус перемещается в диалог при открытии, блокируется внутри,Escapeзакрывает, фокус возвращается к триггеру. HTML<dialog>обрабатывает большинство этого нативно в современных браузерах. - Валидация форм с live-сообщениями об ошибках: Валидация на
blurдля отдельных полей, на submit для всех. При ошибке:aria-invalid='true'на инпуте,aria-describedbyуказывает на сообщение об ошибке. Итоговые ошибки:aria-live='assertive'сводка вверху формы, фокус на ней. - ARIA live regions для toast-уведомлений:
<div role='status' aria-live='polite' aria-atomic='true'>объявляет изменения без прерывания текущего чтения.role='alert'- только для критических ошибок. Монтируйте live region в корневом layout и обновляйте его контент при появлении уведомления. - Кастомные дропдауны vs нативный `<select>`: Нативный
<select>полностью доступен везде, обрабатывает всю клавиатурную навигацию. Единственная причина строить кастомный - свобода стилизации. Если строите кастомный - используйте паттерн combobox из APG, тестируйте с VoiceOver и NVDA. - Таблицы данных: Используйте нативные
<table>,<th>сscope='col'/scope='row',<caption>. Избегайте CSS grid или divs для табличных данных.
Заключение
Долг по доступности в большинстве production-приложений возникает из одного источника: интерактивные компоненты на дженерик-элементах, лейблы существующие только визуально, изменения состояния невидимые для дерева доступности, и управление фокусом которое никогда не рассматривалось. По отдельности ничего из этого не сложно исправить - сложно исправить масштабно постфактум.
Практический путь: добавьте axe-core в CI-пайплайн - это час настройки. Добавьте тестирование клавиатурой в определение готовности для любого интерактивного компонента. Используйте нативные HTML-элементы везде где используете <div> - <button>, <a>, <input>, <select>, <details>. Используйте Radix UI или React Aria для сложных виджетов.
Для аудита доступности или доступной frontend-реализации - услуга аудита доступности | технический SEO | обсудить проект.
Источники
- W3C. 'Web Content Accessibility Guidelines (WCAG) 2.2': https://www.w3.org/TR/WCAG22/
- W3C. 'ARIA Authoring Practices Guide (APG)': https://www.w3.org/WAI/ARIA/apg/
- WebAIM. 'Screen Reader User Survey #10 (2024)': https://webaim.org/projects/screenreadersurvey10/
- WHO. 'Disability and Health': https://www.who.int/news-room/fact-sheets/detail/disability-and-health
- MDN Web Docs. 'Accessibility': https://developer.mozilla.org/en-US/docs/Web/Accessibility
- Deque. 'axe-core': https://github.com/dequelabs/axe-core
- Radix UI. 'Accessible component primitives': https://www.radix-ui.com/
