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:

Live Example
Overview Features Pricing

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:

Live Example
Tab 1 Tab 2 Tab 3

Content for tab 1

Variants

Default Variant

Live Example
Tab 1 Tab 2 Tab 3

Default variant with bottom border indicator

Pills Variant

Live Example
Tab 1 Tab 2 Tab 3

Pills variant with filled background for active tab

Underline Variant

Live Example
Tab 1 Tab 2 Tab 3

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:

Live Example
First Second Third

Second tab is active by default

Features

  • ARIA tab patternrole="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--underline on the wrapper
  • Theme-aware — Adapts to all 14 themes

HTML

html 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

javascript 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);
})();

Astro / Svelte: Astro · Svelte