Вертикальные и горизонтальные табы

Таб-интерфейс (tab interface) представляет собой компонент пользовательского интерфейса, позволяющий переключаться между несколькими логически связанными областями контента. В веб-разработке табы реализуются с использованием HTML, CSS и JavaScript, однако корректная доступность такого интерфейса требует дополнительной логики управления фокусом, клавиатурной навигацией и ARIA-атрибутами.

Библиотека ally.js предоставляет набор утилит, предназначенных для работы с доступностью (accessibility) и управлением фокусом. При построении табов она применяется для:

  • управления фокусируемыми элементами;
  • определения текущего активного элемента;
  • ограничения области навигации;
  • корректной обработки клавиатурных событий;
  • соблюдения рекомендаций WAI-ARIA.

В рамках таб-интерфейсов особенно важна корректная поддержка вертикальной и горизонтальной ориентации вкладок, поскольку навигация с клавиатуры отличается.


Основные роли WAI-ARIA для табов

При создании табов необходимо соблюдать спецификацию ARIA Authoring Practices, которая определяет набор ролей и атрибутов.

Контейнер табов

Контейнер, содержащий список вкладок, получает роль:

role="tablist"

Также указывается ориентация:

aria-orientation="horizontal"
aria-orientation="vertical"

Если атрибут не указан, по умолчанию предполагается горизонтальная ориентация.

Элемент вкладки

Каждая вкладка получает:

role="tab"

Дополнительные атрибуты:

  • aria-selected="true" — активная вкладка
  • aria-controls="panel-id" — связь с панелью контента
  • tabindex="0" — доступность для фокуса

Неактивные вкладки обычно получают tabindex="-1".

Панель вкладки

Контент вкладки определяется ролью:

role="tabpanel"

Атрибуты:

  • aria-labelledby="tab-id"
  • hidden или aria-hidden="true" для неактивных панелей

Базовая HTML-структура табов

Пример минимальной структуры:

<div class="tabs">
  <div role="tablist" aria-orientation="horizontal">
    <button role="tab" id="tab-1" aria-controls="panel-1" aria-selected="true">
      Профиль
    </button>

    <button role="tab" id="tab-2" aria-controls="panel-2" aria-selected="false">
      Настройки
    </button>

    <button role="tab" id="tab-3" aria-controls="panel-3" aria-selected="false">
      Безопасность
    </button>
  </div>

  <div id="panel-1" role="tabpanel" aria-labelledby="tab-1">
    Контент профиля
  </div>

  <div id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>
    Контент настроек
  </div>

  <div id="panel-3" role="tabpanel" aria-labelledby="tab-3" hidden>
    Контент безопасности
  </div>
</div>

Управление фокусом с помощью ally.js

Одной из ключевых задач таб-интерфейса является правильное управление фокусом клавиатуры. Библиотека ally.js предоставляет модуль:

ally.query.focusable

Он позволяет определить все элементы, способные получать фокус.

Пример:

const tabs = ally.query.focusable({
  context: document.querySelector('[role="tablist"]')
});

Полученный список содержит все вкладки, участвующие в навигации.


Переключение вкладок

Для переключения активной вкладки необходимо:

  1. изменить aria-selected;
  2. изменить tabindex;
  3. скрыть и показать панели;
  4. переместить фокус.

Пример функции переключения:

function activateTab(tab) {
  const tablist = tab.closest('[role="tablist"]');
  const tabs = tablist.querySelectorAll('[role="tab"]');

  tabs.forEach(t => {
    t.setAttribute('aria-selected', 'false');
    t.setAttribute('tabindex', '-1');
  });

  tab.setAttribute('aria-selected', 'true');
  tab.setAttribute('tabindex', '0');

  const panels = document.querySelectorAll('[role="tabpanel"]');
  panels.forEach(panel => panel.hidden = true);

  const panel = document.getElementById(tab.getAttribute('aria-controls'));
  panel.hidden = false;

  tab.focus();
}

Горизонтальные табы

Горизонтальные вкладки располагаются в строку. Навигация осуществляется клавишами:

  • ArrowRight — следующая вкладка
  • ArrowLeft — предыдущая вкладка
  • Home — первая вкладка
  • End — последняя вкладка

Обработка клавиатуры:

function handleHorizontalNavigation(event, tabs, index) {
  if (event.key === 'ArrowRight') {
    event.preventDefault();
    const next = (index + 1) % tabs.length;
    tabs[next].focus();
  }

  if (event.key === 'ArrowLeft') {
    event.preventDefault();
    const prev = (index - 1 + tabs.length) % tabs.length;
    tabs[prev].focus();
  }
}

Для определения текущего фокуса может использоваться:

ally.get.activeElement();

Этот метод корректно работает даже в сложных случаях, включая Shadow DOM.


Вертикальные табы

Вертикальные вкладки располагаются колонкой. Основное отличие — использование других клавиш:

  • ArrowDown — следующая вкладка
  • ArrowUp — предыдущая вкладка

Контейнер табов должен иметь атрибут:

aria-orientation="vertical"

Пример разметки:

<div role="tablist" aria-orientation="vertical">
  <button role="tab">Общие</button>
  <button role="tab">Аккаунт</button>
  <button role="tab">Уведомления</button>
</div>

Универсальная обработка ориентации

Для поддержки обеих ориентаций необходимо определить тип навигации.

function getOrientation(tablist) {
  return tablist.getAttribute('aria-orientation') || 'horizontal';
}

Обработчик клавиатуры:

function handleKeydown(event) {
  const tab = event.target;
  const tablist = tab.closest('[role="tablist"]');
  const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
  const index = tabs.indexOf(tab);

  const orientation = getOrientation(tablist);

  if (orientation === 'horizontal') {
    handleHorizontalNavigation(event, tabs, index);
  }

  if (orientation === 'vertical') {
    handleVerticalNavigation(event, tabs, index);
  }
}

Вертикальная навигация:

function handleVerticalNavigation(event, tabs, index) {
  if (event.key === 'ArrowDown') {
    event.preventDefault();
    const next = (index + 1) % tabs.length;
    tabs[next].focus();
  }

  if (event.key === 'ArrowUp') {
    event.preventDefault();
    const prev = (index - 1 + tabs.length) % tabs.length;
    tabs[prev].focus();
  }
}

Ограничение области фокуса

При сложных интерфейсах вкладки могут находиться внутри модальных окон или других компонентов. В таких случаях применяется механизм focus trapping.

ally.js предоставляет инструмент:

ally.maintain.tabFocus

Он позволяет ограничить перемещение фокуса внутри заданного контейнера.

Пример:

const handle = ally.maintain.tabFocus({
  context: document.querySelector('.tabs')
});

После завершения работы:

handle.disengage();

Проверка доступности элементов

В некоторых ситуациях вкладка может быть визуально скрыта, но оставаться в DOM. Необходимо исключить её из навигации.

Для этого применяется:

ally.is.focusable(element)

Пример:

const availableTabs = tabs.filter(tab => ally.is.focusable(tab));

Синхронизация состояния вкладок и панелей

При переключении вкладок важно синхронизировать три состояния:

  1. ARIA-атрибуты
  2. видимость панели
  3. позицию фокуса

Нарушение этой синхронизации приводит к проблемам с экранными дикторами.

Правильная последовательность действий:

  1. снять активность со всех вкладок;
  2. установить aria-selected="true" у новой;
  3. скрыть панели;
  4. показать нужную панель;
  5. переместить фокус.

Ленивое отображение контента

Иногда панели загружаются только при активации вкладки. В этом случае важно сохранить корректные ARIA-связи.

Пример:

if (!panel.dataset.loaded) {
  loadContent(panel);
  panel.dataset.loaded = true;
}

При этом aria-controls должен существовать заранее.


Управление фокусом при удалении вкладок

Если вкладка удаляется динамически, необходимо определить новый элемент фокуса. ally.js предоставляет функцию:

ally.get.activeElement()

Пример обработки:

const active = ally.get.activeElement();

if (!document.contains(active)) {
  tabs[0].focus();
}

Адаптивные интерфейсы

В адаптивных интерфейсах горизонтальные вкладки могут превращаться в вертикальные при уменьшении ширины экрана.

Пример:

if (window.innerWidth < 600) {
  tablist.setAttribute('aria-orientation', 'vertical');
} else {
  tablist.setAttribute('aria-orientation', 'horizontal');
}

Обработчики клавиатуры автоматически используют правильную навигацию.


Стилизация вертикальных табов

Вертикальная ориентация обычно реализуется через flex-layout.

[role="tablist"][aria-orientation="vertical"] {
  display: flex;
  flex-direction: column;
}

Горизонтальный вариант:

[role="tablist"][aria-orientation="horizontal"] {
  display: flex;
}

Активная вкладка:

[role="tab"][aria-selected="true"] {
  font-weight: bold;
  border-bottom: 2px solid currentColor;
}

Типичные ошибки реализации

Отсутствие tabindex

Без tabindex вкладки могут быть недоступны для клавиатуры.

Неправильная клавиатурная навигация

Использование стрелок без учета ориентации нарушает рекомендации ARIA.

Несинхронизированные панели

Если панель отображается, но вкладка не имеет aria-selected="true", экранные дикторы сообщают некорректную информацию.

Отсутствие управления фокусом

При переключении вкладки фокус должен оставаться на вкладке, а не переходить в панель.


Интеграция с другими модулями ally.js

При работе с табами могут использоваться дополнительные возможности библиотеки:

Определение текущего фокуса

ally.get.activeElement()

Поиск фокусируемых элементов

ally.query.focusable()

Проверка фокусируемости

ally.is.focusable()

Ограничение области фокуса

ally.maintain.tabFocus()

Комбинация этих инструментов позволяет создавать полностью доступные интерфейсы вкладок как для горизонтального, так и для вертикального расположения.