Tabs — Vanilla
Tabs with vanilla HTML + JS. Same structure and behavior as Astro Tabs. Ensure Rizzo CSS is loaded.
Tabs
Tabbed panels that match the Astro Tabs component: same markup (span tabs, tabs__panels-wrapper, data-tab-id / data-panel-id), class-based active state (tabs__tab--active, tabs__panel--active), and full keyboard support (Arrow keys, Home, End, Enter, Space).
Basic Usage
Provide panel content in the same order as the tabs. Same example as Astro and Svelte:
Overview
This is the overview content. It provides a general introduction to the topic.
With Content Property
You can put panel content in each panel’s tabs__panel-content div (or inject HTML via JS). Same labels as Astro/Svelte:
Content for tab 1
Variants
Default Variant
Default variant with bottom border indicator
Pills Variant
Pills variant with filled background for active tab
Underline Variant
Underline variant with thicker bottom border for active tab
Default Tab
Specify which tab is active by default by setting the correct initial aria-selected, tabindex, and tabs__tab--active / tabs__panel--active in HTML, and pass that tab’s data-tab-id to your init script. Same example as Astro/Svelte:
Second tab is active by default
Features
- ARIA tab pattern —
role="tablist",role="tab",role="tabpanel",aria-selected,aria-controls,aria-labelledby,aria-hidden - Keyboard — Arrow Right/Down, Arrow Left/Up, Home, End, Enter, Space
- Three variants — Default,
tabs--pills,tabs--underlineon the wrapper - Theme-aware — Adapts to all 14 themes
HTML
<div class="tabs" data-tabs="my-tabs">
<div class="tabs__list" role="tablist" aria-label="Tabs">
<span class="tabs__tab tabs__tab--active" id="my-tabs-tab-tab1" role="tab" tabindex="0" aria-selected="true" aria-controls="my-tabs-panel-tab1" data-tab-id="tab1" data-tab-index="0">Tab 1</span>
<span class="tabs__tab" id="my-tabs-tab-tab2" role="tab" tabindex="-1" aria-selected="false" aria-controls="my-tabs-panel-tab2" data-tab-id="tab2" data-tab-index="1">Tab 2</span>
<span class="tabs__tab" id="my-tabs-tab-tab3" role="tab" tabindex="-1" aria-selected="false" aria-controls="my-tabs-panel-tab3" data-tab-id="tab3" data-tab-index="2">Tab 3</span>
</div>
<div class="tabs__panels-wrapper">
<div class="tabs__panel tabs__panel--active" id="my-tabs-panel-tab1" role="tabpanel" aria-labelledby="my-tabs-tab-tab1" aria-hidden="false" data-panel-id="tab1" data-panel-index="0">
<div class="tabs__panel-content">Panel 1 content.</div>
</div>
<div class="tabs__panel" id="my-tabs-panel-tab2" role="tabpanel" aria-labelledby="my-tabs-tab-tab2" aria-hidden="true" data-panel-id="tab2" data-panel-index="1">
<div class="tabs__panel-content">Panel 2 content.</div>
</div>
<div class="tabs__panel" id="my-tabs-panel-tab3" role="tabpanel" aria-labelledby="my-tabs-tab-tab3" aria-hidden="true" data-panel-id="tab3" data-panel-index="2">
<div class="tabs__panel-content">Panel 3 content.</div>
</div>
</div>
</div> JavaScript
(function initTabs() {
var tabsId = 'my-tabs'; // match data-tabs on your container
var activeTabId = 'tab1'; // initial active tab id
var container = document.querySelector('[data-tabs="' + tabsId + '"]');
if (!container) return;
var tabButtons = container.querySelectorAll('[role="tab"]');
var tabPanels = container.querySelectorAll('[role="tabpanel"]');
if (tabButtons.length === 0 || tabPanels.length === 0) return;
var activeIndex = [].slice.call(tabButtons).findIndex(function(btn) {
return btn.getAttribute('data-tab-id') === activeTabId;
});
if (activeIndex === -1) activeIndex = 0;
function activateTab(index) {
if (index < 0 || index >= tabButtons.length) return;
var targetButton = tabButtons[index];
var targetTabId = targetButton.getAttribute('data-tab-id');
if (!targetTabId) return;
[].forEach.call(tabButtons, function(btn, idx) {
var isActive = idx === index;
btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
btn.setAttribute('tabindex', isActive ? '0' : '-1');
btn.classList.toggle('tabs__tab--active', isActive);
});
[].forEach.call(tabPanels, function(panel) {
var panelId = panel.getAttribute('data-panel-id');
var isActive = panelId === targetTabId;
panel.setAttribute('aria-hidden', isActive ? 'false' : 'true');
panel.classList.toggle('tabs__panel--active', isActive);
});
}
[].forEach.call(tabButtons, function(button, index) {
button.addEventListener('click', function() { activateTab(index); });
button.addEventListener('keydown', function(e) {
var targetIndex = index;
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
targetIndex = (index + 1) % tabButtons.length;
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
targetIndex = index === 0 ? tabButtons.length - 1 : index - 1;
break;
case 'Home':
e.preventDefault();
targetIndex = 0;
break;
case 'End':
e.preventDefault();
targetIndex = tabButtons.length - 1;
break;
case 'Enter':
case ' ':
e.preventDefault();
activateTab(index);
return;
default:
return;
}
activateTab(targetIndex);
tabButtons[targetIndex].focus();
});
});
activateTab(activeIndex);
})();