Клавиатура и фокус: как сделать сайт, которым можно пользоваться без мыши
Порядок фокуса, видимый индикатор, ссылка «Перейти к содержимому», липкие шапки, модальные окна на dialog и составные виджеты

Часть 4 из 8 серии «Доступный веб на практике». Предыдущая часть: Доступные формы: подписи, подсказки, ошибки и автозаполнение.
Простая проверка: уберите мышь и попробуйте пройти главный сценарий своего сайта клавишей Tab. Большинство проблем найдётся за пять минут. В этой части — как сделать навигацию с клавиатуры удобной и предсказуемой.
Клавиатура и фокус
Порядок фокуса
Фокус должен двигаться в логическом порядке — обычно таком же, как визуальный. Порядок определяется порядком элементов в DOM, поэтому:
- не переставляйте элементы визуально через
order,flex-direction: row-reverseили абсолютное позиционирование так, что порядок табуляции перестаёт совпадать с порядком чтения; - не используйте положительный
tabindex(tabindex="1","2"...). Он ломает естественный порядок для всей страницы.
tabindex нужен только в двух значениях:
| Значение | Когда |
|---|---|
tabindex="0" |
Сделать фокусируемым кастомный интерактивный элемент (если без него никак) |
tabindex="-1" |
Сделать элемент фокусируемым программно (element.focus()), но не через Tab: заголовок после перехода, контейнер ошибок, модальное окно |
Видимый фокус
Никогда не делайте так без замены:
/* ❌ Убивает навигацию с клавиатуры */
*:focus { outline: none; }
Если стандартная обводка не нравится дизайнеру — замените её, используя :focus-visible. Этот псевдокласс срабатывает при навигации с клавиатуры, но не при клике мышью:
/* ✅ Заметный фокус только для клавиатуры */
:focus-visible {
outline: 3px solid #1a5fb4;
outline-offset: 2px;
}
/* Убираем обводку при клике мышью там, где браузер её всё же рисует */
:focus:not(:focus-visible) {
outline: none;
}
Индикатор фокуса должен быть контрастен к фону (не менее 3:1) и заметен по площади. Если вы рисуете фокус через box-shadow, добавьте прозрачный outline — он станет видимым в режиме высокой контрастности Windows, где тени отключаются:
.button:focus-visible {
box-shadow: 0 0 0 3px #1a5fb4;
outline: 3px solid transparent; /* проявится в forced-colors */
}
Ссылка «Перейти к содержимому»
Без неё пользователь клавиатуры на каждой странице проходит Tab'ом всё меню.
<a class="skip-link" href="#main">Перейти к содержимому</a>
...
<main id="main" tabindex="-1">...</main>
.skip-link {
position: absolute;
left: 1rem;
top: -100px;
padding: 0.5rem 1rem;
background: #fff;
color: #000;
z-index: 1000;
}
.skip-link:focus {
top: 1rem;
}
Ссылка невидима, пока не получит фокус — первым же нажатием Tab.
Фокус не должен прятаться (WCAG 2.4.11)
Классическая ситуация: липкая шапка. Пользователь жмёт Tab, страница прокручивается, и элемент в фокусе оказывается под шапкой. Решение в одну строку:
html {
scroll-padding-top: 5rem; /* высота липкой шапки + запас */
}
То же касается баннеров cookies и плавающих чатов: они не должны перекрывать элемент в фокусе.
Ловушки фокуса
Фокус не должен застревать в элементе без возможности выйти клавиатурой (критерий 2.1.2). Типичные виновники — встроенные плееры, виджеты карт, iframe сторонних сервисов, кастомные редакторы.
Единственное место, где фокус должен удерживаться внутри, — модальное окно. Но и там Esc должен его закрывать.
Модальное окно на <dialog>
Раньше доступная модалка требовала десятков строк кода: ловушка фокуса, скрытие фона от скринридера, Esc, возврат фокуса. Сегодня всё это даёт нативный <dialog> с методом showModal():
<button type="button" id="open-dialog">Записаться на консультацию</button>
<dialog id="booking" aria-labelledby="booking-title">
<h2 id="booking-title">Запись на консультацию</h2>
<form method="dialog">
<label for="b-name">Имя</label>
<input id="b-name" name="name" autocomplete="name" required>
<button value="submit">Записаться</button>
<button value="cancel" formnovalidate>Отмена</button>
</form>
</dialog>
const opener = document.getElementById('open-dialog');
const dialog = document.getElementById('booking');
opener.addEventListener('click', () => dialog.showModal());
// Современные браузеры возвращают фокус сами, но явный возврат надёжнее
dialog.addEventListener('close', () => opener.focus());
showModal() делает остальную страницу инертной (недоступной для фокуса и скринридера), переносит фокус внутрь, закрывается по Esc, а ::backdrop даёт затемнение фона. Подробнее — MDN: <dialog>.
Для боковых панелей и других случаев, когда часть страницы нужно временно «выключить», есть атрибут inert:
<main inert>...</main> <!-- нельзя сфокусировать, скрыто от скринридера -->
Составные виджеты: клавиши-стрелки
Вкладки, меню, списки выбора, деревья работают по-другому: Tab заходит в виджет один раз, а внутри движение — стрелками (так называемый «roving tabindex»). Если вы пишете такой виджет сами, сверяйтесь с ARIA Authoring Practices Guide — там есть шаблон и ожидаемая клавиатурная модель для каждого паттерна. А лучше — возьмите проверенную библиотеку (см. Доступность в React: id, фокус при смене маршрута и объявления).
Серия «Доступный веб на практике»
- Доступность сайта: для кого она на самом деле и с чего начать
- Семантический HTML и альтернативный текст: фундамент доступного сайта
- Доступные формы: подписи, подсказки, ошибки и автозаполнение
- Клавиатура и фокус: как сделать сайт, которым можно пользоваться без мыши — вы здесь
- Контраст, анимация и размер кнопок: визуальная доступность сайта
- ARIA и динамический контент: когда атрибуты помогают, а когда вредят
- Доступность в React: id, фокус при смене маршрута и объявления
- Как тестировать доступность: автоматика, ручная проверка и чеклист
Следующая часть: Контраст, анимация и размер кнопок: визуальная доступность сайта
Agama Labs — разработка сайтов и веб-приложений. Если хотите проверить доступность своего сайта, напишите нам: hello@agamalabs.com


