Навигация с помощью клавиши Tab — основной способ перемещения по интерфейсу для пользователей клавиатуры. Когда она ломается, страдают не только люди с ограниченными возможностями, но и все, кто привык работать без мыши: опытные пользователи, операторы ввода данных, те, кто использует ноутбук в дороге. В React-приложениях проблемы с Tab возникают чаще, чем в статичной вёрстке, потому что компоненты динамически монтируются, скрываются и перерисовываются, а фокус браузера об этом не всегда «знает».
Главный принцип, который стоит держать в голове: порядок табуляции должен следовать визуальному и логическому порядку интерфейса, а фокус никогда не должен теряться или попадать в недоступные элементы. Всё остальное — частные случаи нарушения этого правила. Ниже разберём самые частые ошибки, их причины и рабочие способы исправления.
- Как браузер определяет порядок Tab
- Ошибка 1. Положительные значения tabindex
- Что делать вместо этого
- Ошибка 2. Кастомные интерактивные элементы без роли и tabindex
- Как исправлять
- Ошибка 3. Потеря фокуса после перерисовки или удаления элемента
- Решение: управляемый перевод фокуса
- Ошибка 4. Ломаные ловушки фокуса в модальных окнах
- Как реализовать корректно
- Ошибка 5. Скрытые, но доступные для Tab элементы
- Ошибка 6. Пропуск и дублирование стопов из-за сторонних компонентов
- Ошибка 7. Нестандартное поведение в составных виджетах
- Как диагностировать проблемы навигации
- Таблица: симптом, причина, решение
- Типичные ошибки при исправлении
- Сценарии: что делать в вашей ситуации
- Практические рекомендации
Как браузер определяет порядок Tab
Чтобы понимать, почему что-то идёт не так, полезно вспомнить базовый механизм. Браузер обходит элементы в таком порядке:
- сначала элементы с положительным значением атрибута tabindex — от меньшего к большему;
- затем интерактивные элементы без tabindex (кнопки, ссылки, поля ввода) в порядке следования в DOM;
- элементы с tabindex=»0″ попадают в общий поток вместе с нативно интерактивными;
- элементы с отрицательным tabindex (обычно -1) из обхода исключаются, но на них можно перевести фокус программно через метод focus().
Отсюда следует практический вывод: если порядок Tab в вашем приложении странный, почти всегда виноват либо ручной tabindex, либо несоответствие между порядком элементов в DOM и их визуальным расположением.
Ошибка 1. Положительные значения tabindex
Классическая ошибка — расставить tabindex=»1″, tabindex=»2″ и так далее, чтобы «настроить» нужный порядок. Это работает до первого изменения макета. Добавили новое поле, переставили блок, открыли компонент на другом разрешении экрана — и порядок табуляции разошёлся с визуальным, причём у разных пользователей по-разному.
Правило простое: не используйте положительный tabindex вообще. Если порядок не устраивает — меняйте порядок элементов в DOM или используйте CSS-свойства порядка отображения так, чтобы они не расходились с логической последовательностью. Помните, что flex-свойство order и grid-размещение меняют только визуальное положение: DOM-порядок остаётся прежним, и именно по нему пойдёт Tab.
Что делать вместо этого
- Убедитесь, что структура JSX соответствует желаемому порядку чтения и табуляции.
- Если визуальный порядок отличается из-за CSS, пересмотрите разметку: чаще всего проще изменить структуру, чем бороться с расхождением.
- Проверьте страницу, пройдясь по ней только с клавиатурой: фокус должен двигаться сверху вниз и слева направо так же, как читается контент.
Ошибка 2. Кастомные интерактивные элементы без роли и tabindex
Вторая по частоте проблема: разработчик делает кликабельный элемент из div или span с обработчиком onClick. Мышью он нажимается, а клавиатурой — нет: div не получает фокус при Tab и не реагирует на Enter и пробел.
Типичный пример — карточка товара, «кнопка» в шапке, раскрывающийся аккордеон, собранный из заголовка с onClick. Для пользователя клавиатуры такой элемент просто не существует.
Как исправлять
- Предпочитайте нативные элементы. Если элемент ведёт себя как кнопка — используйте , как ссылка — . Нативные элементы бесплатно получают фокус, обработку Enter/Space, роль и озвучивание скринридером.
- Если семантический элемент невозможен (например, вся карточка кликабельна), добавьте tabindex=»0″, роль через атрибут role и обработчики клавиатуры: onKeyDown с проверкой кодов Enter и Space.
- Не забывайте про состояние: для переключателей нужна роль switch или checkbox и атрибут aria-checked, для выпадающих списков — соответствующие ARIA-паттерны.
Отдельный нюанс: вложенная интерактивность. Если внутри кликабельной карточки лежит настоящая кнопка «В избранное», пользователь клавиатуры получит два соседних стопа табуляции, но мышью нажмёт одно из двух случайно. Продумайте заранее, какой элемент главный, и сделайте его единственной целью Tab.
Ошибка 3. Потеря фокуса после перерисовки или удаления элемента
Специфика React: когда компонент, содержащий сфокусированный элемент, размонтируется или условно скрывается, браузер сбрасывает фокус на тело документа. Пользователь жмёт Tab дальше — и попадает в начало страницы. Для человека, работающего вслепую, это дезориентация: он внезапно «оказался» в другом месте интерфейса.
Типичные сценарии:
- удаление строки таблицы или пункта списка, где была кнопка действия;
- переключение вкладок, при котором панель с активным элементом исчезает;
- условный рендеринг блока формы после отправки;
- фильтрация списка, после которой выбранный элемент пропал.
Решение: управляемый перевод фокуса
После удаления или замены элемента программно переместите фокус на логичную цель. Элемент-приёмник помечается tabindex=»-1″, чтобы он не добавлял лишний стоп в обход Tab, но принимал программный фокус:
- Определите осмысленную цель: заголовок обновлённого списка, следующая строка, кнопка закрытия уведомления.
- Добавьте ей tabIndex={-1} и ref.
- В момент изменения состояния вызовите ref.current.focus().
Удобный паттерн — хранить в состоянии идентификатор элемента, которому нужно вернуть фокус, и обрабатывать это в useEffect после перерисовки. Так вы не привязываетесь к конкретному месту в дереве компонентов.
Ошибка 4. Ломаные ловушки фокуса в модальных окнах
Модальное окно — самое проблемное место для клавиатурной навигации. Правильное поведение выглядит так: при открытии фокус попадает внутрь окна, Tab циклически ходит только по его элементам, Escape закрывает окно, при закрытии фокус возвращается на элемент, который был активен до открытия.
Частые нарушения:
- Фокус остаётся под оверлеем. Окно открыто, но Tab продолжает водить по затемнённой странице. Пользователь взаимодействует с невидимыми элементами.
- Фокус не переносится внутрь при открытии. Скринридер продолжает читать фон, пользователь не понимает, что появилось диалоговое окно.
- Нет возврата фокуса при закрытии. После закрытия модалки Tab начинается с начала страницы.
- Ловушка без выхода. Обратная ситуация: фокус заперт внутри, но кнопка закрытия недоступна с клавиатуры или Escape не работает.
Как реализовать корректно
- При монтировании окна сохраните document.activeElement — это точка возврата.
- Переведите фокус на первый интерактивный элемент окна или на сам контейнер с tabindex=»-1″.
- Повесьте обработчик keydown: при Tab находите первый и последний фокусируемые элементы внутри окна и зацикливайте переход; при Escape вызывайте закрытие.
- При размонтировании верните фокус на сохранённый элемент.
Если проект уже использует библиотеку компонентов, проверьте, поддерживает ли её компонент модального окна эти четыре поведения. Большинство зрелых решений (Radix UI, Headless UI, Reach UI и подобные) делают это из коробки — тогда задача сводится к тому, чтобы не обходить их механизмы своими обработчиками.
Ошибка 5. Скрытые, но доступные для Tab элементы
Обратная ситуация: элемент невидим на экране, но всё ещё получает фокус. Пользователь жмёт Tab, «ничего не происходит», хотя фокус ушёл куда-то за пределы видимой области. Причины бывают разные:
- скрытие через opacity: 0 или смещение за экран вместо display: none — такие элементы остаются в порядке табуляции;
- карусели и слайдеры, где неактивные слайды спрятаны визуально, но присутствуют в DOM;
- аккордеоны, где свёрнутая панель скрыта только высотой (max-height: 0) без visibility или hidden-атрибута;
- отрисованные заранее шаги многошаговой формы.
Исправление зависит от подхода. Надёжные варианты: использовать условный рендеринг (React просто не создаёт элемент), применять атрибут hidden или свойство visibility: hidden, которые убирают элемент из табуляции, либо добавлять tabIndex={-1} и inert-подобное поведение для неактивных областей. Атрибут inert, поддерживаемый современными браузерами, отключает и фокус, и взаимодействие сразу для целого поддерева — удобно для неактивных слайдов карусели.
Ошибка 6. Пропуск и дублирование стопов из-за сторонних компонентов
Сторонние виджеты — редакторы текста, карты, календари, капчи — часто вносят собственную логику фокуса. Возможные сюрпризы: виджет захватывает Tab внутри себя и не выпускает наружу, наоборот — требует десятка нажатий, чтобы пройти свои внутренние кнопки, или вставляет в поток невидимые служебные элементы.
Что проверить при выборе и подключении такого компонента:
- документирует ли автор поведение клавиатуры и есть ли настройки управления фокусом;
- можно ли выйти из виджета клавиатурой (стандартный паттерн — выход по Escape);
- сколько стопов табуляции занимает виджет и можно ли сократить их группировкой внутренних элементов;
- что происходит с фокусом при инициализации: некоторые виджеты сами забирают фокус при загрузке страницы, что раздражает пользователей.
Ошибка 7. Нестандартное поведение в составных виджетах
Для сложных компонентов вроде меню, комбобоксов, таб-панелей и радиогрупп веб-платформа предполагает особую модель: внутри группы Tab ведёт к самой группе, а между её опциями пользователь перемещается стрелками. Реализация «в лоб», где каждая опция отдельный стоп Tab, формально работает, но резко увеличивает число нажатий и противоречит ожиданиям пользователей скринридеров.
Практические ориентиры:
- радиокнопки: один стоп Tab на всю группу, переключение стрелками — это даёт нативный input type=»radio» с общим name;
- меню: Tab открывает меню, стрелки ходят по пунктам, Escape закрывает;
- табы: Tab переводит на список вкладок, стрелки переключают активную вкладку;
- списки с выбором (listbox): активный элемент управляется стрелками, фокус единый.
Реализовать эти паттерны вручную непросто, поэтому разумнее взять готовые headless-компоненты, где логика уже написана, а внешний вид остаётся за вами.
Как диагностировать проблемы навигации
Большинство ошибок находится вручную за несколько минут. Порядок проверки:
- Отключите мышь физически или просто не трогайте её. Пройдите по странице от адресной строки до конца, отмечая каждый стоп Tab.
- Сверяйте: совпадает ли порядок фокуса с визуальным порядком? Нет ли «невидимых» стопов? Нет ли элементов, до которых Tab не доходит?
- Проверьте, всегда ли видно, где фокус: стандартная рамка может быть удалена стилями. Если вы убираете outline, замените его другим заметным индикатором — например, через псевдокласс focus-visible.
- Протестируйте ключевые сценарии: открытие и закрытие модалки, удаление элемента списка, отправку формы, переключение вкладок.
- Полезно включить отображение фокуса принудительно или использовать расширения браузера, подсвечивающие текущий активный элемент.
Из инструментов автоматизации помогают аудиты доступности в Lighthouse и линтеры вроде eslint-plugin-jsx-a11y: они ловят очевидные вещи — кликабельные div без роли, картинки без alt, неправильное использование aria-атрибутов. Но автоматика не проверит логику порядка и поведение фокуса в динамике, поэтому ручной проход с клавиатурой остаётся обязательным.
Таблица: симптом, причина, решение
| Симптом | Вероятная причина | Решение |
|---|---|---|
| Элемент не получает фокус по Tab | Кликабельный div/span без tabindex и роли | Заменить на button/a или добавить tabindex=»0″, роль и обработку клавиш |
| Порядок Tab хаотичный | Положительные значения tabindex | Убрать их, привести порядок DOM к визуальному |
| После действия фокус «исчезает» | Элемент с фокусом размонтирован | Программный focus() на логичную цель с tabindex=»-1″ |
| Tab уходит под открытую модалку | Нет ловушки фокуса | Зациклить Tab внутри окна, вернуть фокус при закрытии |
| «Пустые» нажатия Tab | Скрытые элементы остаются в потоке | Условный рендеринг, hidden, visibility, inert |
| Непонятно, где фокус | Убран outline без замены | Стили для :focus-visible с заметным индикатором |
Типичные ошибки при исправлении
- Гасить outline полностью. Удаление рамки фокуса «для красоты» лишает пользователя клавиатуры единственного ориентира. Используйте :focus-visible, чтобы индикатор показывался только при клавиатурной навигации.
- Вешать tabindex=»0″ на всё подряд. Абзацы и заголовки с tabindex создают шум: пользователь вынужден пролистывать неинтерактивный текст. Табулируются только интерактивные элементы.
- Перехватывать Tab глобально. Попытка «улучшить» навигацию через document-level обработчик обычно ломает ожидаемое поведение во всех остальных местах страницы.
- Забывать про мобильные сценарии. На сенсорных устройствах Tab нет, но та же логика фокуса влияет на экранные дикторы и автозаполнение форм, поэтому исправления полезны и там.
Сценарии: что делать в вашей ситуации
Вы унаследовали проект с хаотичной навигацией. Не пытайтесь починить всё сразу. Начните с критических путей: форма оформления заказа, вход, основное действие продукта. Пройдите их с клавиатурой, зафиксируйте список дефектов и устраняйте по одному, начиная с потери фокуса и недоступных кнопок.
Вы пишете новый компонент. Сразу закладывайте правильную основу: нативные элементы, отсутствие положительных tabindex, обработка удаления и скрытия. Дешевле сделать верно изначально, чем потом ретроспективно чинить десятки мест.
Вы выбираете UI-библиотеку. Проверьте демо каждого ключевого компонента с клавиатурой прямо на сайте документации: модалка, выпадающий список, табы, диалог подтверждения. Поведение там — хороший предиктор поведения в вашем проекте.
Проблема воспроизводится только в одном браузере. Тогда сначала исключите свою логику: убедитесь, что порядок DOM корректен и нет ручного управления фокусом, конфликтующего с поведением браузера. Расхождения между браузерами чаще всего касаются деталей — например, того, какие элементы считаются фокусируемыми, поэтому опирайтесь на явные tabindex и программный фокус, а не на предположения о дефолтах.
Практические рекомендации
- Держите порядок табуляции равным порядку DOM и визуальному порядку — это снимает большинство проблем.
- Никогда не используйте положительный tabindex.
- Любой кликабельный нестандартный элемент делайте фокусируемым и отзывчивым на Enter и пробел.
- После любого изменения интерфейса, затрагивающего активный элемент, явно управляйте фокусом.
- Модальные окна реализуйте с полным циклом: перенос фокуса внутрь, ловушка, Escape, возврат фокуса.
- Регулярно проходите ключевые сценарии приложения только с клавиатурой — это самый быстрый регрессионный тест навигации.
Конкретный следующий шаг: откройте свой проект, пройдите с клавиатурой один основной пользовательский путь от загрузки до целевого действия и запишите каждое место, где фокус повёл себя неожиданно. Этот список — ваш план работ, отсортированный по реальной важности, а не по гипотезам.
