Паттерн roving tabindex

Библиотека Tabbable в JavaScript предназначена для работы с элементами страницы, которые могут получать фокус с помощью клавиши Tab. Она предоставляет утилиты для определения фокусируемых элементов, фильтрации видимых элементов и работы с последовательностью фокуса в сложных интерфейсах. Одним из ключевых паттернов, реализуемых с её помощью, является roving tabindex.

Паттерн roving tabindex позволяет управлять фокусом в пределах группы элементов, сохраняя при этом стандартное поведение клавиши Tab для всего документа. В отличие от простого управления фокусом через tabindex="0" и tabindex="-1", roving tabindex динамически изменяет активный элемент в группе, обеспечивая удобную навигацию с клавиатуры и соблюдение принципов доступности (ARIA).


Принцип работы roving tabindex

  1. Один активный элемент В группе элементов только один элемент получает tabindex="0", все остальные — tabindex="-1". Это гарантирует, что при переходе по Tab пользователь попадёт именно на этот элемент.

  2. Динамическое смещение фокуса При навигации с помощью стрелок (ArrowUp, ArrowDown, ArrowLeft, ArrowRight) фокус перемещается между элементами группы. Предыдущий элемент получает tabindex="-1", новый — tabindex="0".

  3. Сохранение стандартного Tab Вне группы поведение клавиши Tab не меняется. Это важно для доступности, чтобы пользователь мог покинуть группу и продолжить навигацию по странице.


Определение фокусируемых элементов с Tabbable

Функция 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, так как навигация строится на этих элементах.


Инициализация 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

Для соответствия стандартам доступности необходимо использовать атрибуты 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>

Особые случаи и рекомендации

  1. Динамическое добавление элементов При изменении DOM список элементов нужно обновлять через tabbable(container) и корректировать tabindex.

  2. Фокус вне группы Если пользователь покидает группу через Tab, активный элемент сохраняется или сбрасывается по логике интерфейса.

  3. Смешанные типы элементов Можно объединять кнопки, ссылки и кастомные элементы, главное, чтобы они были фокусируемыми (tabindex="-1" для неактивных).

  4. Производительность Для больших списков лучше кэшировать результат tabbable(container) и обновлять его только при необходимости, чтобы избежать лишних обходов DOM.


Примеры расширенной логики

  • Поддержка Home/End для быстрого перехода к первому и последнему элементу.
  • Поддержка PageUp/PageDown в сетках и таблицах.
  • Сохранение фокуса после динамического фильтра или сортировки элементов.

Итоговое поведение

Использование Tabbable в сочетании с паттерном roving tabindex обеспечивает:

  • Полный контроль над фокусом в интерактивных группах
  • Естественную клавиатурную навигацию
  • Соответствие стандартам доступности ARIA
  • Лёгкую интеграцию с динамическими интерфейсами

Реализация roving tabindex через Tabbable становится основой для создания сложных компонентов, таких как меню, вкладки, списки элементов и сетки, которые полностью управляемы с клавиатуры и корректно работают с экранными читалками.