Навигация с клавиатуры и фокус
Если по странице нельзя пройти клавишей Tab и понять, где ты находишься, — для части пользователей она сломана.
Фокус — это элемент, который сейчас «активен» для клавиатуры: на него реагируют нажатия, по нему перемещается пользователь без мыши.
Зачем это на практике
Мышь есть не у всех. Люди с нарушением моторики, незрячие (скринридер ходит по тем же фокусируемым элементам), да и обычные пользователи при заполнении формы — все нажимают Tab, чтобы перейти к следующему полю, и Shift+Tab, чтобы вернуться. Кнопки и ссылки активируют Enter или Пробел. Если ваш интерфейс это не поддерживает, целый класс людей не сможет им пользоваться.
Tab-порядок и почему он зависит от разметки
Браузер обходит фокусируемые элементы в том порядке, в каком они идут в HTML-коде (в DOM), а не в каком расположены на экране. По умолчанию фокус получают только интерактивные элементы: ссылки <a href>, кнопки, поля форм, <select>, <textarea>.
Отсюда практический вывод: если вы CSS-ом переставили блоки местами (например, через flex или grid), визуальный порядок и порядок Tab могут разойтись — пользователь будет «прыгать» по экрану хаотично. Поэтому держите логичный порядок прямо в разметке.
<!-- Порядок Tab здесь: ссылка → поле → кнопка, ровно как в коде -->
<a href="/home">На главную</a>
<input type="text" name="q">
<button>Найти</button>
tabindex: 0, -1 и почему не положительный
Атрибут tabindex управляет фокусируемостью. У него три осмысленных режима:
| Значение | Что делает |
tabindex="0" | Делает элемент фокусируемым по Tab в естественном порядке (по месту в DOM). Нужно для кастомных виджетов на <div>. |
tabindex="-1" | Элемент нельзя достичь по Tab, но фокус можно поставить из JavaScript (element.focus()). Для программного управления фокусом. |
tabindex="1" и больше | Положительный — антипаттерн, см. ниже. |
Почему не положительный. Положительный tabindex вырывает элемент из естественного порядка и ставит в начало Tab-обхода (сначала идут все элементы с положительным значением по возрастанию, и лишь потом — обычные). На реальной странице это превращается в хаос: добавили одно поле — и весь порядок надо пересчитывать вручную. Правило: используйте только 0 и -1.
<!-- Кастомная «кнопка» на div: делаем её фокусируемой через tabindex=0 -->
<div role="button" tabindex="0">Развернуть</div>
<!-- Заголовок, на который мы переведём фокус из JS после открытия модалки -->
<h2 tabindex="-1">Настройки</h2>
Видимый фокус — это не для красоты
Когда фокус на элементе, браузер рисует вокруг него контур (focus ring). Пользователь клавиатуры по нему понимает, где он сейчас. Иногда дизайнеры просят его убрать, потому что «некрасиво»:
/* НИКОГДА так не делайте: пользователь клавиатуры слепнет */
:focus { outline: none; }
Без видимого фокуса навигация с клавиатуры становится «угадайкой». Если стандартный контур не вписывается в дизайн — не убирайте его, а замените на свой заметный стиль. Современный приём — :focus-visible: он показывает кольцо при навигации с клавиатуры и прячет при клике мышью:
/* Своя заметная обводка только когда пришли с клавиатуры */
:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
Skip link — пропуск-ссылка
На каждой странице сверху обычно лежит шапка с большим меню. Пользователь скринридера или клавиатуры вынужден Tab-ать через десятки пунктов меню на каждой странице, прежде чем добраться до текста. Утомительно.
Skip link («перейти к содержимому») — первая ссылка в коде. Обычно она спрятана и появляется только при получении фокуса:
<body>
<a class="skip-link" href="#main">Перейти к содержимому</a>
<header>...большое меню...</header>
<main id="main">
...основной контент...
</main>
</body>
Первое нажатие Tab ставит фокус на эту ссылку; Enter перепрыгивает сразу к <main>, минуя меню. Чтобы ссылка не мешала визуально, её прячут за экран и возвращают по фокусу:
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 8px;
top: 8px;
}
Ловушка фокуса в модальных окнах
Когда открыто модальное окно (диалог), фокус должен оставаться внутри него. Иначе по Tab можно «уехать» на кнопки за окном, которые визуально перекрыты, — пользователь клавиатуры запутается окончательно. Это называют focus trap (удержание фокуса).
Правильное поведение модалки складывается из нескольких частей:
- При открытии — перевести фокус внутрь окна (на заголовок или первое поле), запомнив, откуда пришли.
- Пока окно открыто —
Tabс последнего элемента возвращает на первый, аShift+Tabс первого — на последний (фокус «зациклен» внутри). - Клавиша
Escapeзакрывает окно. - При закрытии — вернуть фокус на ту кнопку, что окно открыла.
Хорошая новость: нативный элемент <dialog> с методом showModal() делает удержание фокуса и реакцию на Escape за вас. Это ещё один довод за нативные элементы.
<dialog id="confirm">
<h2>Удалить файл?</h2>
<button>Отмена</button>
<button>Удалить</button>
</dialog>
<!-- В JS: confirm.showModal() — фокус удержится внутри, Escape закроет -->
Как это работает под капотом
Браузер ведёт упорядоченный список фокусируемых элементов — так называемый tab order. По умолчанию в него попадают только интерактивные элементы в порядке DOM. tabindex="0" добавляет в этот список неинтерактивный элемент по его месту в DOM; tabindex="-1" убирает из списка, но оставляет доступным для focus(); положительный tabindex создаёт отдельную «очередь вперёд», которая обходится раньше всего и ломает естественный порядок. Focus trap в модалке — это перехват события keydown на Tab: скрипт сам решает, куда поставить фокус, не давая ему покинуть окно.
Частые ошибки
outline: noneбез замены. Самая частая и болезненная ошибка — пользователь клавиатуры теряет, где он.- Положительный
tabindex. Ломает порядок обхода и плодит баги при каждом изменении вёрстки. - Кликабельные
<div>безtabindex="0". Мышью работает, с клавиатуры — нет. - Модалка без удержания фокуса. По Tab уезжаешь на фон за окном.
- Визуальный порядок ≠ порядок DOM. Переставили блоки CSS-ом — Tab «прыгает» по экрану нелогично.
Итоги
- Порядок Tab определяется порядком в DOM, поэтому держите его логичным в разметке.
- Используйте только
tabindex="0"(добавить в обход) иtabindex="-1"(фокус из JS); положительные значения — антипаттерн. - Никогда не убирайте видимый фокус без замены; удобный инструмент —
:focus-visible. - Skip link позволяет перепрыгнуть навигацию и сразу попасть к содержимому.
- В модальных окнах удерживайте фокус внутри, закрывайте по
Escapeи возвращайте фокус назад; нативный<dialog>делает многое сам.