Табы (вкладки) — эффективный способ организовать контент на странице без перезагрузки. Чистый JavaScript даёт полный контроль над поведением, анимациями и доступностью. Вот базовая структура, которая работает:

class Tabs { constructor(container) { this.container = container; this.tabs = container.querySelectorAll('[role="tab"]'); this.panels = container.querySelectorAll('[role="tabpanel"]'); this.init(); } init() { this.tabs.forEach(tab => { tab.addEventListener('click', e => this.switchTab(e)); tab.addEventListener('keydown', e => this.handleKeydown(e)); }); } switchTab(e) { // Логика переключения вкладок } handleKeydown(e) { // Управление с клавиатуры }
}

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

Правильная разметка — основа работоспособности. Используйте семантические атрибуты для доступности:

<div class="tabs" role="tablist"> <button role="tab" aria-selected="true" aria-controls="tab-1" id="tab-btn-1">Вкладка 1</button> <button role="tab" aria-selected="false" aria-controls="tab-2" id="tab-btn-2">Вкладка 2</button> <div role="tabpanel" id="tab-1" aria-labelledby="tab-btn-1">Контент 1</div> <div role="tabpanel" id="tab-2" aria-labelledby="tab-btn-2" hidden>Контент 2</div>
</div>

Полная JavaScript-реализация

Рабочий код с обработкой событий, управлением состоянием и доступностью:

document.addEventListener('DOMContentLoaded', () => { const tabContainers = document.querySelectorAll('[role="tablist"]'); tabContainers.forEach(container => { const tabs = container.querySelectorAll('[role="tab"]'); const panels = container.querySelectorAll('[role="tabpanel"]'); tabs.forEach(tab => { tab.addEventListener('click', e => { e.preventDefault(); // Деактивируем все табы и панели tabs.forEach(t => { t.setAttribute('aria-selected', 'false'); t.setAttribute('tabindex', '-1'); }); panels.forEach(p => p.hidden = true); // Активируем текущий таб и панель tab.setAttribute('aria-selected', 'true'); tab.removeAttribute('tabindex'); const panelId = tab.getAttribute('aria-controls'); const panel = document.getElementById(panelId); panel.hidden = false; }); // Навигация с клавиатуры tab.addEventListener('keydown', e => { const index = Array.from(tabs).indexOf(tab); switch(e.key) { case 'ArrowLeft': const prev = index > 0 ? tabs[index - 1] : tabs[tabs.length - 1]; prev.focus(); prev.click(); break; case 'ArrowRight': const next = index < tabs.length - 1 ? tabs[index + 1] : tabs[0]; next.focus(); next.click(); break; case 'Home': tabs[0].focus(); tabs[0].click(); break; case 'End': tabs[tabs.length - 1].focus(); tabs[tabs.length - 1].click(); break; } }); }); });
});

Стилизация и CSS

Базовые стили для визуального выделения активного состояния:

.tabs [role="tablist"] { display: flex; border-bottom: 1px solid #ccc;
} .tabs [role="tab"] { padding: 10px 20px; border: none; background: none; cursor: pointer; border-bottom: 2px solid transparent;
} .tabs [role="tab"][aria-selected="true"] { border-bottom-color: #007bff; color: #007bff;
} .tabs [role="tabpanel"] { padding: 20px; border: 1px solid #ccc; border-top: none;
}

Типичные ошибки и как их избежать

Большинство проблем возникают из-за неправильного управления состоянием и отсутствия обработки крайних случаев.

Ошибка Решение
Дублирование обработчиков при динамическом добавлении Используйте делегирование событий на контейнере
Некорректная работа с клавиатурой Реализуйте полную навигацию по стрелкам, Home, End
Отсутствие fallback при отключённом JS Показывайте весь контент по умолчанию, скрывайте через JS
Неправильные ARIA-атрибуты Следуйте официальной спецификации WAI-ARIA

Сравнение подходов: чистый JS vs библиотеки

Выбор зависит от сложности проекта и требований к производительности.

  • Чистый JavaScript: полный контроль, нулевые зависимости, лучшая производительность
  • : простота реализации, кроссплатформенность, но лишний вес
  • React/Vue компоненты: переиспользуемость, интеграция с состоянием приложения

Чек-лист реализации

Проверьте эти пункты перед запуском:

  • Все табы управляются с клавиатуры (стрелки, Home, End)
  • ARIA-атрибуты корректно обновляются при переключении
  • Активное состояние визуально выделено
  • Контент скрыт/показан через hidden, а не display: none
  • Обработчики не дублируются при динамических изменениях
  • Есть fallback для случаев без JavaScript

Динамическое добавление и удаление табов

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

document.querySelector('[role="tablist"]').addEventListener('click', e => { if (e.target.getAttribute('role') === 'tab') { // Общая логика переключения const tab = e.target; const panelId = tab.getAttribute('aria-controls'); // Сброс состояний document.querySelectorAll('[role="tab"]').forEach(t => { t.setAttribute('aria-selected', 'false'); }); document.querySelectorAll('[role="tabpanel"]').forEach(p => { p.hidden = true; }); // Активация текущего tab.setAttribute('aria-selected', 'true'); document.getElementById(panelId).hidden = false; }
});

Частые вопросы

Какие основные ошибки допускают при создании табов?

Частые ошибки: отсутствие обработки неактивных состояний, неправильное управление фокусом для accessibility, дублирование обработчиков событий при динамическом добавлении табов и отсутствие fallback для JavaScript.

Можно ли сделать табы без JavaScript?

Да, с помощью CSS и чекбоксов можно создать простые табы, но функциональность будет ограничена: отсутствуют события, сложная логика переходов и динамическое управление состоянием.

Как сделать табы доступными для скринридеров?

Используйте ARIA-атрибуты: aria-selected для активной вкладки, aria-controls для связи вкладки с контентом, role="tablist" для контейнера и управляйте фокусом с клавиатуры.

Как обновлять табы при динамическом добавлении контента?

Используйте делегирование событий: вешайте обработчик на контейнер табов, а не на отдельные элементы. Это позволяет работать с динамически добавленными табами без переинициализации.