Skip to content

Веб-доступность в 2026: скринридеры, WCAG 2.2 и практики, которые реально работают

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

Веб-доступность: скринридер, клавиатурная навигация, диаграмма WCAG 2.2 и ARIA roles
Опубликовано:Обновлено:Время чтения:22 мин чтения

В прошлом году я три дня аудировал 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 | обсудить проект.

Источники

Похожие статьи

Архитектура веб-производительности: системный анализ 12 инженерных принципов (издание 2026)

Глубокий технический разбор веб-производительности 2026 года: физика сетей, конвейеры изображений, модели выполнения JavaScript, Critical Rendering Path, edge-вычисления, Core Web Vitals и продакшн RUM - с реальными бенчмарками и конкретными примерами реализации.

EngineeringArchitectureCore Web Vitals
Читать статью

React 19: useOptimistic, use(), Server Actions - механизм и архитектура (2026)

Детальный разбор пяти новых примитивов React 19: useOptimistic (конкурентная composition оптимистичного состояния с автоматическим rollback), use() (условное чтение Promise и Context), Server Actions (RPC-over-HTTP контракт и Progressive Enhancement), useActionState (стейт-threading паттерн), useFormStatus. Action-based mutation архитектура, которая заменяет Redux/Zustand для серверных мутаций.

React 19ArchitectureNext.js
Читать статью

Partial Prerendering (PPR) в продакшне: архитектурные паттерны (2026)

Детальный разбор Next.js Partial Prerendering в продакшне. Механизм двухфазного ответа (статический shell с CDN + стриминговые dynamic holes), правила размещения Suspense-границ, взаимодействие с Full Route Cache, дизайн fallback для нулевого CLS, измеренные TTFB/LCP, сравнение с ISR+CSR и full SSR, известные ограничения и фреймворк принятия решений.

Next.jsPerformancePPR
Читать статью