Симптом обычно обнаруживается поздно: форма отправляется мышью, но после Tab фокус исчезает на светлом фоне, поле «Почта» в дереве браузера не получает имени, а после submit остаётся только красная рамка. Цена не в одном CSS-правиле. Пользователь не понимает, где находится и что исправлять, а команда получает дефект уже после того, как разметка разошлась по нескольким экранам.
Для первого прохода не нужен большой аудит всего продукта. Берём одну форму, один путь с клавиатуры и три наблюдаемых свойства: виден ли фокус, есть ли у control роль и имя, остаётся ли ошибка текстом рядом с полем. Затем смотрим это в настоящем браузере, а не объявляем DOM-домысел доказательством. Такой маршрут подходит для фронтенд-проекта 2019 года: он не требует отдельной платформы, но оставляет проверяемый результат в задаче.
Сначала фиксируем пользовательский сбой
Запись «доступность не готова» слишком широкая: по ней нельзя выбрать правку. В тестовом сценарии лучше назвать конкретную операцию. Например: открыть форму профиля, перейти Tab в поле почты, оставить поле пустым, отправить форму, понять по фокусу и тексту, какое действие нужно дальше. Если один из шагов не воспроизводится, это уже не общая оценка, а дефект в маршруте.
В этом сценарии DOM нужен, но его мало. Инспектор покажет, что у input есть id и класс. Он не заменяет последовательный переход фокуса, вычисленное имя control и видимое состояние после ошибки. Семантика формируется нативным HTML, атрибутами и текущим состоянием страницы; браузер показывает её через реальное поведение и своё дерево доступности. Поэтому я сначала прохожу страницу клавиатурой, а потом открываю DevTools для конкретного активного элемента.
| Симптом | Вероятная причина | Что увидеть в браузере | Первая правка |
|---|---|---|---|
| После Tab непонятно, где курсор | outline отключён или визуальный индикатор не отличается от фона | активный элемент меняется, но видимой рамки или заливки нет | вернуть заметный :focus-стиль и перепроверить на реальном фоне |
| Поле имеет текст рядом, но имени нет | видимая подпись сделана span или div без связи с input | в дереве браузера у textbox пустое или неверное name | связать label и input через for/id либо вложить control в label |
| После submit есть только красная граница | ошибка не описана текстом или состояние aria-invalid не синхронизировано | у поля нет понятного error text, а ошибочный элемент не отмечен | вывести текст ошибки, установить aria-invalid и оставить связь с полем |
Таблица не обещает, что одна проверка закроет все особенности вспомогательных технологий. Она отделяет очевидную поломку интерфейса от предположения. Пустое имя у textbox можно исправить разметкой. Слышимость динамического сообщения уже требует отдельной проверки с выбранной технологией, и без такого прогона нельзя писать, что сообщение точно прозвучало.
Минимальная форма: имя, описание и ошибка
В следующей фикстуре подпись связана с input через for/id. Подсказка и место для ошибки перечислены в aria-describedby; при неверном значении код синхронно меняет aria-invalid, делает текст ошибки видимым и возвращает фокус в поле только после неудачной отправки. Код не является сертификатом доступности. Он делает отношения в разметке явными, чтобы их можно было проверить.
<form id="profile-form" novalidate>
<label for="profile-email">Рабочая почта</label>
<p id="profile-email-hint">Нужен адрес, на который придёт ссылка.</p>
<input id="profile-email" type="email" required
aria-describedby="profile-email-hint profile-email-error"
aria-errormessage="profile-email-error" aria-invalid="false" />
<p id="profile-email-error" hidden></p>
<button type="submit">Сохранить</button>
</form>
const form = document.querySelector("#profile-form");
const email = document.querySelector("#profile-email");
const error = document.querySelector("#profile-email-error");
function setEmailError(message) {
const invalid = Boolean(message);
email.setAttribute("aria-invalid", String(invalid));
error.hidden = !invalid;
error.textContent = message;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const message = email.validity.valid ? "" : "Введите адрес в формате name@example.com";
setEmailError(message);
if (message) email.focus();
});
У label есть конкретная задача: HTML связывает caption с labelable form control по for/id или вложению control в label. Рядом стоящий span такой связи сам по себе не создаёт. Это полезная граница для код-ревью: не обсуждаем, похоже ли слово на подпись, а проверяем, может ли браузер сопоставить его с полем.
В примере error element присутствует рядом с полем, но скрыт, пока значения достаточно. Когда валидация нашла проблему, текст становится видимым, а aria-invalid получает true. WAI-ARIA 1.1 отдельно связывает aria-errormessage с aria-invalid и требует не прятать актуальный текст. Даже при такой разметке я не выдаю факт объявления сообщения за доказанный: его подтверждает отдельный ручной прогон с нужной вспомогательной технологией.
Клавиатурный маршрут, который можно вложить в задачу
Маршрут ниже безопасен для локального стенда или тестового контура: он ничего не отправляет на сервер и не требует учётной записи. Цель — записать факты до правки и после неё. Если форма зависит от сети, подставляем тестовый ответ или отключаем submit, но не делаем вывод по одному скриншоту DOM.
- Открыть страницу в поддерживаемом проектом браузере в чистом состоянии. Не ставить курсор мышью внутрь формы: начать последовательную навигацию с начала документа.
- Нажимать Tab и записывать порядок: ссылка пропуска блока, поле, следующее поле, кнопка. На каждом шаге проверить, что индикатор фокуса различим на настоящем фоне и что фокус не уходит на декоративный элемент.
- Нажать Shift+Tab из кнопки и убедиться, что обратный путь возвращает к ожидаемому control. Это ловит разрыв порядка, который иногда скрывает положительный tabindex.
- Оставить обязательное поле пустым, активировать submit с клавиатуры и записать, куда попал фокус. Если команда переводит фокус в поле или summary, это должно быть одним предсказуемым решением, а не случайным побочным эффектом обработчика.
- Проверить, что рядом есть текст ошибки с понятной причиной, а не только цвет, и что исходная подсказка не исчезла без замены. В примере пользователь видит формат адреса, который ожидает форма.
- Открыть панель Accessibility для активного поля и сверить role, name, description, invalid state и текст ошибки. Зафиксировать браузер и версию в задаче, потому что это наблюдение конкретного user agent.
Почему проверяем браузер, а не только DOM
DOM-дерево отвечает на вопрос, какие узлы создал код. Пользовательский путь отвечает на другой вопрос: к какому узлу можно последовательно прийти, что видно при фокусе и какое имя браузер вычислил для control. Эти ответы могут расходиться. Например, input с id существует в DOM, но внешняя подпись не связана с ним; div c click-обработчиком виден мышью, но его клавиатурная модель зависит от дополнительной реализации.
В DevTools выбираем именно элемент, на котором стоит фокус после нужного шага. Затем смотрим роль, имя, описание и состояние. У обычного input type=email ожидается нативная роль textbox/field в представлении конкретного браузера; не надо добавлять role только ради похожего слова. Проверяем, что name совпадает с человеческой подписью «Рабочая почта», а description содержит подсказку и актуальную ошибку там, где браузер её показывает.
Такой снимок дерева не равен тесту со screen reader. Он помогает поймать пустое имя, лишний aria-label или потерянную связь до дорогого ручного прогона. Последний шаг требует выбрать браузер и вспомогательную технологию из поддерживаемой матрицы проекта, пройти тот же маршрут и записать наблюдение отдельно. В этой статье такой прогон не выполнялся.
Фокус — часть действия, а не декоративная рамка
WCAG 2.1 требует, чтобы при последовательной навигации порядок фокуса сохранял смысл и работоспособность операции. Отдельно для интерфейса, которым управляют с клавиатуры, нужен режим с видимым индикатором фокуса. Поэтому :focus не стоит заменять на пустое outline:none без равноценной видимой альтернативы. В обычной форме чаще всего выгоднее сохранить порядок документа и нативные элементы, чем собирать новый порядок через положительные tabindex.
Положительный tabindex создаёт собственный относительный порядок фокусировки. На короткой странице это может казаться удобной правкой: поле «поднимают» перед кнопкой. Через месяц рядом появляется ещё один control, и маршрут перестаёт совпадать с визуальным порядком. Для начального ремонта сначала убрать искусственные номера, вернуть нормальную DOM-последовательность и пройти путь Tab ещё раз.
Границы этой проверки
- Этот маршрут не измеряет контраст, масштабирование текста, работу жестов, язык страницы и все критерии WCAG. Он закрывает один узкий риск: базовое управление формой с клавиатуры и понятный текст ошибки.
- Панель Accessibility зависит от браузера и версии. Результат нужно прикладывать как наблюдение конкретного браузера, а не выдавать за универсальное описание всех accessibility API.
- Наличие aria-атрибута не гарантирует полезный опыт. Если пользовательский текст плохой или ошибка появляется в неожиданном месте, дерево может выглядеть формально полным, а сценарий всё равно останется запутанным.
- Автор не выполнял здесь прогон со screen reader и не утверждает, что динамическая ошибка была произнесена. План такого прогона должен повторить тот же сценарий после согласования тестовой среды.
Итог: оставить после правки проверяемый маршрут
Для базовой доступности формы я не начинаю с набора ARIA-атрибутов. Сначала называю поломку: пропал фокус, у control нет имени или ошибка не стала текстом. Затем прохожу клавиатурный маршрут, смотрю активный элемент и его представление в браузере, правлю нативную семантику и повторяю маршрут. Это небольшой объём работы, но он связывает разметку с реальным действием пользователя и не прячет дефект за красивой рамкой.
Проверяемые источники
- WCAG 2.1: 2.4.3 Focus Order — при последовательной навигации порядок фокуса должен сохранять смысл и работоспособность операции
- WCAG 2.1: 2.4.7 Focus Visible — для управляемого с клавиатуры интерфейса нужен режим с видимым индикатором фокуса
- WCAG 2.1: 3.3.1 Error Identification — если ошибка ввода определяется автоматически, ошибочный элемент и текст ошибки должны быть определены для пользователя
- HTML Standard: label element — label связывается с form control через for/id или вложением самого control
- HTML Standard: tabindex — положительный tabindex создаёт отдельный относительный порядок последовательной фокусировки
- WAI-ARIA 1.1: aria-describedby — список ID связывает элемент с полной текстовой description
- WAI-ARIA 1.1: aria-errormessage — ссылка на пользовательское сообщение об ошибке используется вместе с aria-invalid; актуальный текст ошибки не должен быть скрыт