Обход страницы клавишей Tab

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

Один проход по странице и не более пяти её собственных таблиц стилей. Ничего не сохраняется.

Или попробуйте mozilla.org, wikipedia.org

Фокус: курсор клавиатуры

Мышью можно указать куда угодно. Клавиатура в каждый момент может «стоять» только на одном элементе, и это положение называется фокусом. Tab двигает его вперёд, Shift+Tab назад, Enter переходит по ссылке, пробел нажимает кнопку. Так перемещаются люди с тремором или ограниченной подвижностью рук. А также пользователи программ экранного доступа, люди с кнопочными устройствами или голосовым управлением и все, кто быстро заполняет формы.

Браузер определяет, что может получить фокус, только по HTML. Ссылки с href, кнопки, поля форм, <summary>, медиаплееры с элементами управления и фреймы входят в маршрут по умолчанию, в том порядке, в каком они идут в исходном коде. Всё остальное пропускается, если атрибут tabindex не говорит иначе.

Три значения tabindex

ЗначениеДействиеКогда применять
tabindex="0"Добавляет элемент в маршрут на его естественное местоНестандартные элементы управления, которые не могут быть настоящей <button>
tabindex="-1"Убирает его из маршрута; скрипты всё ещё могут передать ему фокусНеактивные вкладки, пункты меню, к которым переходят стрелками, слайды карусели за пределами экрана
tabindex="1" и большеСтавит его впереди всех остальных, начиная с наименьшего числаПочти никогда. Фокус скачет по экрану

Обход следует этим правилам буквально: сначала положительные значения по возрастанию, затем порядок исходного кода. Группа переключателей с одинаковым name считается одной остановкой, потому что внутри группы фокус перемещают стрелки. Отключённые элементы, закрытые <dialog>, области с inert или hidden и содержимое закрытых <details> не учитываются.

Что отмечает проверка

Кликабельные элементы, недоступные с клавиатуры
<div onclick> или элемент с role="button" без tabindex. Пользователь мыши может его нажать, а пользователь клавиатуры даже не может до него добраться. Замените его на <button type="button">: фокус, Enter и пробел достаются бесплатно.
Ссылки без href
Для браузера <a> без адреса это обычный текст, и Tab проходит мимо.
Элементы с tabindex="-1"
Отмечаются, если только ссылка не дублирует другую доступную ссылку на тот же адрес и не входит в список вкладок, меню, панель инструментов или похожую группу, где управление берут на себя стрелки.
Ссылка пропуска
Проверка ищет среди первых трёх остановок ссылку на якорь на той же странице, например <a href="#content">Skip to content</a>, и убеждается, что целевой id существует. Она также считает, сколько нажатий нужно от верха страницы до <main>. Если их пять или меньше, отсутствие ссылки пропуска отмечается лишь для сведения.
Контур фокуса
Мы ищем в блоках <style> страницы, встроенных стилях и максимум пяти таблицах стилей с того же сайта правила вроде a:focus { outline: none } или * { outline: 0 }. Если контур ничем не заменён в стилях :focus-visible или :focus, находка красная: пользователи клавиатуры не видят, где находятся.
Другие остановки, на которые стоит взглянуть
Фокусируемые элементы внутри aria-hidden="true", остановки без доступного имени, фреймы без title и ссылки на # или javascript:, которые работают как кнопки.

Как вернуть видимый фокус

Дизайнеры часто убирают контур, потому что он появляется после клика мышью и кажется им некрасивым. Селектор :focus-visible закрывает этот спор. Он срабатывает, только когда браузер решает, что пользователю нужно видеть фокус, а на практике это означает работу с клавиатуры.

:focus-visible {
  outline: 3px solid #1a5fb4;
  outline-offset: 2px;
}

WCAG 2.2 требует индикатор с контрастом не ниже 3:1 по отношению к окружающим цветам. Шаблон :focus:not(:focus-visible) { outline: none } безопасен, и о нём не сообщается.

Пределы статического чтения

Маршрут вычисляется по HTML, который получает наш сервер. Обработчики кликов, подключённые в JavaScript через addEventListener, не оставляют следов в разметке, поэтому <div>, подключённый таким образом, не обнаруживается. Меню, построенные скриптом, здесь тоже не видны, как и клавиатурные ловушки, когда фокус попадает в виджет и не может из него выйти. Таблицы стилей с другого домена или сверх пятой не читаются.

Закончите настоящей проверкой, она займёт две минуты. Отложите мышь, загрузите страницу и нажимайте Tab, пока не дойдёте до подвала. Вы всегда должны видеть, где находитесь, и должны суметь открыть каждое меню и отправить каждую форму.

Частые вопросы

Как самому проверить навигацию с клавиатуры?

Щёлкните в адресной строке, затем снова и снова нажимайте Tab. Shift+Tab возвращает назад, Enter работает на ссылках, пробел на кнопках и флажках, стрелки внутри групп переключателей и меню, а Escape закрывает диалоги. В Safari на macOS сначала включите в дополнительных настройках «Выделять каждый объект на веб-странице при нажатии Tab».

Достаточно ли tabindex="0" на div, чтобы сделать его кнопкой?

Нет. Элемент становится фокусируемым, но Enter и пробел ничего не делают, пока вы не добавите обработчики клавиш, а программа экранного доступа не назовёт его кнопкой без role="button". Настоящая <button> даёт всё три сразу.

Нужна ли ссылка пропуска, если на странице есть элемент main?

Да, для зрячих пользователей клавиатуры. Программы экранного доступа умеют переходить к ориентиру <main> сочетанием клавиш. У того, кто пользуется только клавишей Tab, такого сочетания нет, и без ссылки пропуска ему приходится проходить каждый пункт меню.

Почему порядок Tab не совпадает с тем, что я вижу на экране?

Tab следует порядку исходного HTML, а не визуальной раскладке. CSS вроде order, flex-direction: row-reverse, размещения в сетке или абсолютного позиционирования может переместить элементы на экране, не меняя их места в исходном коде. Положительный tabindex даёт тот же эффект.

Всегда ли outline: none нарушает доступность?

Только если ему нет замены. Правило, которое убирает контур и в том же блоке задаёт при фокусе видимую рамку, фон или тень, вполне допустимо, и эта проверка засчитывает его как замену.