Библиотека Tabbable в JavaScript предназначена для работы с элементами страницы, которые могут получать фокус с помощью клавиши Tab. Она предоставляет утилиты для определения фокусируемых элементов, фильтрации видимых элементов и работы с последовательностью фокуса в сложных интерфейсах. Одним из ключевых паттернов, реализуемых с её помощью, является roving tabindex.
Паттерн roving tabindex позволяет управлять фокусом
в пределах группы элементов, сохраняя при этом стандартное поведение
клавиши Tab для всего документа. В отличие от простого управления
фокусом через tabindex="0" и tabindex="-1",
roving tabindex динамически изменяет активный элемент в группе,
обеспечивая удобную навигацию с клавиатуры и соблюдение принципов
доступности (ARIA).
Один активный элемент В группе элементов только
один элемент получает tabindex="0", все остальные —
tabindex="-1". Это гарантирует, что при переходе по Tab
пользователь попадёт именно на этот элемент.
Динамическое смещение фокуса При навигации с
помощью стрелок (ArrowUp, ArrowDown,
ArrowLeft, ArrowRight) фокус перемещается
между элементами группы. Предыдущий элемент получает
tabindex="-1", новый — tabindex="0".
Сохранение стандартного Tab Вне группы поведение клавиши Tab не меняется. Это важно для доступности, чтобы пользователь мог покинуть группу и продолжить навигацию по странице.
Функция tabbable() возвращает массив элементов, которые
могут получить фокус. Она учитывает:
tabindex (положительное значение игнорируется,
0 и отрицательное корректно обрабатываются)display: none,
visibility: hidden, родительские элементы)disabled и
aria-hiddenПример использования:
import { tabbable } from 'tabbable';
const container = document.querySelector('.menu');
const focusableElements = tabbable(container);
Массив focusableElements содержит только те элементы, на
которые можно поставить фокус с клавиатуры. Это ключевой момент при
реализации roving tabindex, так как навигация строится на этих
элементах.
const menuItems = tabbable(container);
// Устанавливаем первый элемент активным
menuItems.forEach((el, index) => {
el.setAttribute('tabindex', index === 0 ? '0' : '-1');
});
Здесь создаётся базовая группа, где только первый элемент доступен через Tab, остальные скрыты для фокусировки.
let currentIndex = 0;
container.addEventListener('keydown', (event) => {
const maxIndex = menuItems.length - 1;
if (event.key === 'ArrowDown') {
event.preventDefault();
menuItems[currentIndex].setAttribute('tabindex', '-1');
currentIndex = currentIndex === maxIndex ? 0 : currentIndex + 1;
menuItems[currentIndex].setAttribute('tabindex', '0');
menuItems[currentIndex].focus();
}
if (event.key === 'ArrowUp') {
event.preventDefault();
menuItems[currentIndex].setAttribute('tabindex', '-1');
currentIndex = currentIndex === 0 ? maxIndex : currentIndex - 1;
menuItems[currentIndex].setAttribute('tabindex', '0');
menuItems[currentIndex].focus();
}
});
Особенности реализации:
preventDefault() блокирует стандартное поведение
стрелок, сохраняя контроль над фокусом.tabindex обеспечивает
корректную работу Tab вне группы.Для соответствия стандартам доступности необходимо использовать атрибуты ARIA:
role="menu" для контейнераrole="menuitem" для элементов группыaria-activedescendant при необходимости связывать
контейнер с активным элементомПример:
<ul class="menu" role="menu">
<li role="menuitem" tabindex="0">Пункт 1</li>
<li role="menuitem" tabindex="-1">Пункт 2</li>
<li role="menuitem" tabindex="-1">Пункт 3</li>
</ul>
Динамическое добавление элементов При изменении
DOM список элементов нужно обновлять через
tabbable(container) и корректировать
tabindex.
Фокус вне группы Если пользователь покидает группу через Tab, активный элемент сохраняется или сбрасывается по логике интерфейса.
Смешанные типы элементов Можно объединять
кнопки, ссылки и кастомные элементы, главное, чтобы они были
фокусируемыми (tabindex="-1" для неактивных).
Производительность Для больших списков лучше
кэшировать результат tabbable(container) и обновлять его
только при необходимости, чтобы избежать лишних обходов DOM.
Использование Tabbable в сочетании с паттерном roving tabindex обеспечивает:
Реализация roving tabindex через Tabbable становится основой для создания сложных компонентов, таких как меню, вкладки, списки элементов и сетки, которые полностью управляемы с клавиатуры и корректно работают с экранными читалками.