Табы (tabs)

Переключаемые панели контента с ARIA tablist/tab/tabpanel — клавиатурная навигация и доступность из коробки.

Задача

Нужен компонент вкладок, где клик по ярлыку переключает контент. Должны работать Tab/стрелки для навигации и screen reader.

Решение

<div class="tabs">
  <div class="tabs__list" role="tablist" aria-label="Разделы">
    <button class="tabs__tab active" role="tab" aria-selected="true"
            aria-controls="panel-1" id="tab-1">
      Описание
    </button>
    <button class="tabs__tab" role="tab" aria-selected="false"
            aria-controls="panel-2" id="tab-2" tabindex="-1">
      Характеристики
    </button>
    <button class="tabs__tab" role="tab" aria-selected="false"
            aria-controls="panel-3" id="tab-3" tabindex="-1">
      Отзывы
    </button>
  </div>

  <div class="tabs__panel" role="tabpanel" id="panel-1" aria-labelledby="tab-1">
    <p>Текст описания продукта.</p>
  </div>
  <div class="tabs__panel" role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
    <p>Технические характеристики.</p>
  </div>
  <div class="tabs__panel" role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>
    <p>Отзывы покупателей.</p>
  </div>
</div>
.tabs__list {
  display: flex;
  gap: 0;
  border-bottom: 2px solid #e2e8f0;
}

.tabs__tab {
  padding: 10px 20px;
  border: none;
  background: none;
  cursor: pointer;
  color: #64748b;
  font-size: 0.95rem;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  transition: color 0.15s, border-color 0.15s;
}

.tabs__tab:hover { color: #1e293b; }

.tabs__tab.active,
.tabs__tab[aria-selected="true"] {
  color: #3b82f6;
  border-bottom-color: #3b82f6;
  font-weight: 600;
}

.tabs__panel { padding: 20px 0; }
const tabList = document.querySelector('[role="tablist"]');
const tabs    = [...tabList.querySelectorAll('[role="tab"]')];

function activate(tab) {
  // Сбросить все
  tabs.forEach((t) => {
    t.classList.remove('active');
    t.setAttribute('aria-selected', 'false');
    t.setAttribute('tabindex', '-1');
    document.getElementById(t.getAttribute('aria-controls')).hidden = true;
  });

  // Активировать выбранный
  tab.classList.add('active');
  tab.setAttribute('aria-selected', 'true');
  tab.removeAttribute('tabindex');
  document.getElementById(tab.getAttribute('aria-controls')).hidden = false;
  tab.focus();
}

tabList.addEventListener('click', (e) => {
  const tab = e.target.closest('[role="tab"]');
  if (tab) activate(tab);
});

// Клавиатурная навигация: стрелки ←/→
tabList.addEventListener('keydown', (e) => {
  const curr = tabs.indexOf(document.activeElement);
  if (curr === -1) return;

  let next;
  if (e.key === 'ArrowRight') next = (curr + 1) % tabs.length;
  if (e.key === 'ArrowLeft')  next = (curr - 1 + tabs.length) % tabs.length;
  if (e.key === 'Home')       next = 0;
  if (e.key === 'End')        next = tabs.length - 1;

  if (next !== undefined) { e.preventDefault(); activate(tabs[next]); }
});

Ключевые моменты

  • role="tablist/tab/tabpanel" + aria-selected + aria-controls — стандартный ARIA-паттерн; screen reader объявит «вкладки».
  • tabindex="-1" на неактивных вкладках — Tab переходит только на активную; стрелки переключают между вкладками.
  • hidden атрибут на панелях — скрывает от screen reader, а не только визуально (display: none).
  • Навигация Home/End — быстрый переход к первой/последней вкладке (ARIA best practices).

Связанные рецепты / темы