Tabs

Tabs are a common UI pattern that benefit from component encapsulation.

Basic Tabs

components/tabs/tabs.jinja
{#def active="" #}
{#css tabs.css #}
{#js tabs.js #}

<div class="tabs" data-tabs {{ attrs.render() }}>
  {{ content }}
</div>
components/tabs/tab-list.jinja
<div class="tab-list" role="tablist">
  {{ content }}
</div>
components/tabs/tab.jinja
{#def id, active=false #}

<button
  role="tab"
  data-tab="{{ id }}"
  aria-selected="{{ 'true' if active else 'false' }}"
  {{ attrs.render(class="tab" ~ (" active" if active else "")) }}
>
  {{ content }}
</button>
components/tabs/tab-panels.jinja
<div class="tab-panels">
  {{ content }}
</div>
components/tabs/tab-panel.jinja
{#def id, active=false #}

<div
  role="tabpanel"
  id="panel-{{ id }}"
  data-panel="{{ id }}"
  {{ attrs.render(class="tab-panel" ~ (" active" if active else "")) }}
  {% if not active %}hidden{% endif %}
>
  {{ content }}
</div>
tabs.js
document.addEventListener('click', (e) => {
  const tab = e.target.closest('[data-tab]');
  if (!tab) return;

  const tabs = tab.closest('[data-tabs]');
  const tabId = tab.dataset.tab;

  // Update tab buttons
  tabs.querySelectorAll('[data-tab]').forEach(t => {
    t.classList.toggle('active', t.dataset.tab === tabId);
    t.setAttribute('aria-selected', t.dataset.tab === tabId);
  });

  // Update panels
  tabs.querySelectorAll('[data-panel]').forEach(p => {
    const isActive = p.dataset.panel === tabId;
    p.classList.toggle('active', isActive);
    p.hidden = !isActive;
  });
});
usage
{#import "tabs/tabs.jinja" as Tabs #}
{#import "tabs/tab-list.jinja" as TabList #}
{#import "tabs/tab.jinja" as Tab #}
{#import "tabs/tab-panels.jinja" as TabPanels #}
{#import "tabs/tab-panel.jinja" as TabPanel #}

<Tabs>
  <TabList>
    <Tab id="overview" active>Overview</Tab>
    <Tab id="features">Features</Tab>
    <Tab id="pricing">Pricing</Tab>
  </TabList>

  <TabPanels>
    <TabPanel id="overview" active>
      <h2>Overview</h2>
      <p>This is the overview content.</p>
    </TabPanel>

    <TabPanel id="features">
      <h2>Features</h2>
      <p>Feature list goes here.</p>
    </TabPanel>

    <TabPanel id="pricing">
      <h2>Pricing</h2>
      <p>Pricing information.</p>
    </TabPanel>
  </TabPanels>
</Tabs>

Vertical Tabs

components/tabs/vertical-tabs.jinja
{#css vertical-tabs.css #}
{#js tabs.js #}

<div class="tabs tabs-vertical" data-tabs {{ attrs.render() }}>
  {{ content }}
</div>
vertical-tabs.css
.tabs-vertical {
  display: flex;
}

.tabs-vertical .tab-list {
  flex-direction: column;
  border-right: 1px solid #ddd;
  border-bottom: none;
}

.tabs-vertical .tab-panels {
  flex: 1;
  padding-left: 1rem;
}

Simple Tabs Component

A higher-level component that takes data:

components/simple-tabs.jinja
{#def tabs, active="" #}
{#css tabs.css #}
{#js tabs.js #}

{% set first_id = tabs[0].id if tabs else "" %}
{% set active_id = active or first_id %}

<div class="tabs" data-tabs>
  <div class="tab-list" role="tablist">
    {% for tab in tabs %}
      <button
        role="tab"
        data-tab="{{ tab.id }}"
        class="tab {{ 'active' if tab.id == active_id else '' }}"
        aria-selected="{{ 'true' if tab.id == active_id else 'false' }}"
      >
        {{ tab.label }}
      </button>
    {% endfor %}
  </div>

  <div class="tab-panels">
    {% for tab in tabs %}
      <div
        role="tabpanel"
        data-panel="{{ tab.id }}"
        class="tab-panel {{ 'active' if tab.id == active_id else '' }}"
        {{ "" if tab.id == active_id else "hidden" }}
      >
        {{ tab.content }}
      </div>
    {% endfor %}
  </div>
</div>
usage
{#import "simple-tabs.jinja" as SimpleTabs #}

<SimpleTabs tabs={{[
  {"id": "tab1", "label": "First", "content": "<p>First tab content</p>"},
  {"id": "tab2", "label": "Second", "content": "<p>Second tab content</p>"},
  {"id": "tab3", "label": "Third", "content": "<p>Third tab content</p>"},
]}} />