Accessibility: создаем инклюзивные веб-приложения
WCAG 2.1, ARIA, keyboard navigation и screen readers
Доступность веб-приложений — это не функция для галочки и не требование регуляторов, которое можно отложить на «после релиза». Это качество кода и качество продукта. По данным ВОЗ, около 16% населения Земли живёт с той или иной формой инвалидности. В России это порядка 11 миллионов человек с официально установленной инвалидностью — и это только те, кто вошёл в статистику. Слабовидящие, пожилые пользователи, люди с временными ограничениями (сломанная рука, солнечный свет на экране телефона) — реальная аудитория, которую неинклюзивный интерфейс отсекает полностью. Для B2B-продуктов доступность приобретает дополнительное измерение. Корпоративные заказчики — банки, госструктуры, крупные ритейлеры — всё чаще включают соответствие WCAG 2.1 в технические требования. Невыполнение этих требований на этапе приёмки означает задержку оплаты, переработки и репутационные потери. Инвестировать в доступность на старте дешевле в 5–10 раз, чем переделывать готовую систему. В этой статье — системный взгляд на accessibility: стандарты, техники, инструменты и типичные ошибки, которые мы видим в проектах с бюджетом от 2 до 10 миллионов рублей. Материал написан для команд, которые хотят делать правильно, а не просто делать. WCAG 2.1: что стоит за аббревиатурой Web Content Accessibility Guidelines — это рекомендации W3C, которые де-факто стали международным стандартом. Текущая актуальная версия — WCAG 2.1, WCAG 2.2 вышел в 2023 году и добавил несколько новых критериев. Версия 3.0 находится в разработке. Вся система строится на четырёх принципах — POUR: Perceivable (Воспринимаемость) — информация должна быть доступна хотя бы через один сенсорный канал. Operable (Управляемость) — интерфейс должен работать без мыши, только с клавиатуры. Understandable (Понятность) — контент и поведение интерфейса должны быть предсказуемы. Robust (Надёжность) — контент должен корректно интерпретироваться вспомогательными технологиями. Каждый принцип содержит критерии успеха, разбитые на три уровня: A — минимальный порог. Несоблюдение делает контент недоступным для части пользователей. Примеры: альтернативный текст для изображений, возможность управления с клавиатуры. AA — стандарт для большинства коммерческих продуктов. Включает контрастность 4.5:1 для обычного текста, наличие видимого фокуса, отсутствие контента, который мигает более трёх раз в секунду. AAA — расширенные требования. Контрастность 7:1, полное аудиоописание видео и т.д. Обычно применяется для специализированных продуктов. Для коммерческого B2B-продукта целевой уровень — AA. Это стандарт, который проверяют при аудите и на который ссылаются в ТЗ. Достичь его реально без архитектурных изменений, если закладывать правильные практики с первой строки кода. Семантика HTML: основа доступности Самая распространённая ошибка в проектах — строить весь интерфейс на дивах и спанах с JavaScript-обработчиками. Это ломает accessibility по умолчанию, потому что скринридер не понимает роль элемента, его состояние и связи с другими элементами. Сравните два подхода: !-- Плохо: потеряна семантика -- div class="btn" onclick="submitForm()" Отправить /div !-- Хорошо: нативный элемент с правильной ролью -- button type="submit" Отправить /button Нативная кнопка получает фокус с клавиатуры, активируется по Enter и Space, имеет роль button для скринридера — бесплатно, без ARIA. Дивовая кнопка требует добавить tabindex="0" , role="button" , обработчики клавиатуры и при этом всё равно будет уступать нативному элементу в надёжности. Ключевые семантические элементы, которые должны использоваться в любом приложении: header , nav , main , footer — структура страницы, лендмарки для навигации скринридером h1 – h6 — иерархия заголовков, по которой пользователи скринридеров ориентируются на странице button для действий, a для навигации — не наоборот label для каждого поля формы с явной связью через for / id table с caption , th scope для табличных данных Практическое правило для команды: если для реализации компонента нужен ARIA, сначала проверьте, нет ли нативного HTML-элемента с нужной семантикой. В большинстве случаев он есть. ARIA: когда нативной семантики недостаточно ARIA (Accessible Rich Internet Applications) — это набор атрибутов, которые добавляют семантику там, где нативного HTML не хватает. Это инструмент для кастомных компонентов: выпадающих меню, вкладок, модальных окон, слайдеров. Первое правило ARIA: не используй ARIA, если можешь обойтись нативным HTML. ARIA работает по трём направлениям: Роли (roles) — что это за элемент: role="dialog" , role="tabpanel" , role="combobox" Свойства (properties) — постоянные характеристики: aria-label , aria-labelledby , aria-describedby , aria-required Состояния (states) — динамические данные: aria-expanded , aria-selected , aria-disabled , aria-checked Пример правильно реализованного аккордеона: div class="accordion" h3 button aria-expanded="false" aria-controls="section1-content" id="section1-header" Раздел первый /button /h3 div id="section1-content" role="region" aria-labelledby="section1-header" hidden p Содержимое раздела... /p /div /div Когда аккордеон открывается, JavaScript меняет aria-expanded="true" и убирает атрибут hidden . Скринридер сообщает пользователю об изменении состояния. Никакой лишней магии — только правильная семантика. Критическая ошибка, которую мы видим в аудитах: aria-label на элементах, у которых уже есть видимый текст. Это создаёт расхождение между тем, что видит обычный пользователь, и тем, что слышит пользователь скринридера. Правило: aria-label используется только когда нет видимого текста. Когда текст есть — используйте aria-labelledby или просто доверяйте контексту. Клавиатурная навигация: полноценный сценарий без мыши По статистике WebAIM (ежегодный опрос пользователей со скринридерами), около 70% из них используют клавиатуру как основной метод ввода. Для людей с двигательными нарушениями клавиатура — единственный способ работы с компьютером. Если ваше приложение нельзя полностью пройти с Tab и Enter, оно недоступно. Базовые требования клавиатурной доступности: Все интерактивные элементы достижимы через Tab Порядок фокуса логически соответствует визуальному порядку Текущий фокус всегда виден — видимый outline не задавлен через outline: none Модальные окна захватывают фокус внутри (focus trap) и возвращают его после закрытия Клавиша Escape закрывает любой оверлей или выпадающий элемент Самая частая ошибка фронтенд-разработчиков — глобальный сброс фокуса в CSS: /* Это ломает accessibility для всех клавиатурных пользователей */ * { outline: none; } /* Правильно: скрывать outline только при навигации мышью */ :focus:not(:focus-visible) { outline: none; } :focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; } Псевдокласс :focus-visible поддерживается всеми современными браузерами. Он активируется при навигации с клавиатуры, но не при клике мышью — это именно то поведение, которое нужно. Для кастомных компонентов важно реализовать правильные паттерны клавиатурного управления согласно спецификации WAI-ARIA Authoring Practices: Tabs (вкладки) : Tab переходит между панелями, стрелки влево/вправо переключают вкладки внутри TabList Menu (меню) : Enter/Space открывает, стрелки навигируют, Escape закрывает Dialog (диалог) : при открытии фокус уходит на первый интерактивный элемент, Tab/Shift+Tab цикличен внутри, Escape закрывает Combobox (поиск с подсказками) : стрелки навигируют по списку, Enter выбирает, Escape сворачивает Скринридеры: как работает реальный пользователь Скринридер — программа, которая озвучивает контент экрана через синтез речи или выводит на брайлевский дисплей. Топ-5 скринридеров по версии WebAIM Survey 2023: JAWS (40%), NVDA (41%), VoiceOver (18%), TalkBack (Android), TalkBack + Braille. JAWS и NVDA — платформы Windows, VoiceOver встроен в macOS и iOS. Скринридер не «видит» пиксели — он читает дерево доступности (accessibility tree), которое браузер строит на основе DOM и ARIA. Понимание этого меняет подход к разработке. Что нужно знать командам: Скрытый контент : display: none и visibility: hidden убирают элемент из дерева доступности. opacity: 0 и позиционирование за пределы экрана — нет. Для визуально скрытых, но доступных элементов используйте паттерн visually-hidden. Динамические изменения : если контент обновляется без перезагрузки страницы (AJAX, уведомления), скринридер не узнает об этом автоматически. Используйте live-регионы: aria-live="polite" для некритичных обновлений, aria-live="assertive" для важных сообщений. Изображения : каждое изображение должно иметь alt . Для декоративных — пустой alt="" , чтобы скринридер пропустил элемент. Для информативных — описание контента, не «фотография» или «изображение». Пример live-региона для уведомлений: !-- Контейнер добавляется в DOM один раз при инициализации -- div aria-live="polite" aria-atomic="true" class="sr-only" id="notifications" /div !-- JavaScript добавляет текст, скринридер озвучивает -- document.getElementById('notifications').textContent = 'Форма успешно отправлена. Мы свяжемся с вами в течение 24 часов.'; CSS-паттерн для visually-hidden элементов (доступны скринридерам, невидимы визуально): .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } Контрастность и визуальная доступность WCAG 2.1 уровня AA требует минимальный коэффициент контрастности 4.5:1 для обычного текста (менее 18pt или 14pt жирного) и 3:1 для крупного текста и графических элементов интерфейса. Это не просто цифры для отчёта — плохая контрастность снижает читаемость для всех пользователей, особенно в условиях яркого освещения. Типичные проблемы в дизайне, которые мы выявляем при аудитах: Серый текст на белом фоне для вторичной информации (часто коэффициент 2.5:1–3:1, не соответствует AA) Placeholder-текст в полях формы с контрастностью ниже 3:1 Текст поверх фотографий без полупрозрачного подложки Иконки без текстовой подписи при контрастности ниже 3:1 Состояния кнопок (disabled, hover, active) без достаточного различия Для проверки контрастности используйте: WebAIM Contrast Checker — быстрая проверка по цветовым кодам Встроенный инструмент в Chrome DevTools (Accessibility → Contrast ratio) Figma-плагины: A11y Annotation Kit, Stark, Color Contrast Analyser Отдельный класс проблем — использование цвета как единственного способа передачи информации. Если статус в таблице обозначен только красным/зелёным кружком без текстового пояснения — это нарушение WCAG 1.4.1 (уровень A). Дальтонизм встречается примерно у 8% мужчин и 0.5% женщин. Решение: добавьте иконку или текстовую метку рядом с цветовым индикатором. Формы: самая сложная зона доступности Формы — основной инструмент взаимодействия в B2B-приложениях: фильтры, поиск, редактирование данных, оформление заказов. Именно здесь концентрируется большинство проблем доступности, потому что формы требуют корректной семантики, правильного управления состоянием и понятной обработки ошибок. Чек-лист доступной формы: Каждое поле имеет видимый label , связанный через for / id или через aria-labelledby . Placeholder не заменяет label — он исчезает при вводе. Обязательные поля помечены явно (текстом «Обязательное поле» или через aria-required="true" ) — не только звёздочкой. Ошибки валидации связаны с конкретным полем через aria-describedby и появляются рядом с полем, а не только в сводке сверху. Фокус перемещается на сообщение об ошибке или первое ошибочное поле после неудачной отправки формы. Группы полей (например, дата рождения из трёх полей) обёрнуты в fieldset с legend . Автозаполнение : поля с autocomplete -атрибутом позволяют пользователям вспомогательных технологий и пользователям с двигательными нарушениями избегать повторного ввода. Пример правильно реализованного поля с ошибкой: div class="field" label for="email" Электронная почта span class="required" aria-hidden="true" * /span span class="sr-only" (обязательное поле) /span /label input type="email" id="email" name="email" autocomplete="email" aria