Skip to content

Tabs

wu-tabs shows one panel at a time under a row of tabs. The kit script adds the keyboard handling and switches the panels. Without the script, the panel without hidden shows.

Class or attribute Effect
wu-tabs Root
wu-tabs-list Part, role="tablist". Row of tabs with a line below, scrolls sideways when narrow; the edge with hidden tabs fades out
wu-tabs-tab Part, a <button role="tab">. Muted text, a brand bar when selected
wu-tabs-panel Part, role="tabpanel". Content of one tab, lined up with the tab labels
aria-selected="true" on a tab Selected tab
hidden on a panel Panel not shown
disabled or aria-disabled="true" on a tab Dimmed, skipped by the arrow keys
card Tabs as joined boxes; the selected one opens into the content below
boxed Tabs and panels inside one bordered box, the selected tab joined to the panel
vertical Tab list at the inline start, panels beside it. Up and Down arrows move between tabs
An <svg> in a tab App icon, sized to the text
animated The bar grows from the center and the panel fades in, opt-in

Click a tab, or focus the selected tab and press the arrow keys, Home or End.

Name, photo and contact details.
<div class="wu-tabs">
<div class="wu-tabs-list" role="tablist" aria-label="Account">
<button class="wu-tabs-tab" type="button" role="tab" id="tabs-profile" aria-controls="tabs-profile-panel" aria-selected="true">Profile</button>
<button class="wu-tabs-tab" type="button" role="tab" id="tabs-billing" aria-controls="tabs-billing-panel" aria-selected="false" tabindex="-1">Billing</button>
<button class="wu-tabs-tab" type="button" role="tab" id="tabs-team" aria-controls="tabs-team-panel" aria-selected="false" tabindex="-1">Team</button>
</div>
<div class="wu-tabs-panel" role="tabpanel" id="tabs-profile-panel" aria-labelledby="tabs-profile" tabindex="0">Name, photo and contact details.</div>
<div class="wu-tabs-panel" role="tabpanel" id="tabs-billing-panel" aria-labelledby="tabs-billing" tabindex="0" hidden>Plan, invoices and payment method.</div>
<div class="wu-tabs-panel" role="tabpanel" id="tabs-team-panel" aria-labelledby="tabs-team" tabindex="0" hidden>Members and their roles.</div>
</div>

The script fills in the roles, the ids, aria-controls, aria-labelledby and tabindex when they are missing, pairing tabs and panels in order. The full markup above also reads correctly before the script runs.

Totals for the selected period.
<div class="wu-tabs">
<div class="wu-tabs-list" aria-label="Report">
<button class="wu-tabs-tab" type="button" aria-selected="true">Summary</button>
<button class="wu-tabs-tab" type="button">Details</button>
<button class="wu-tabs-tab" type="button" disabled>Export</button>
</div>
<div class="wu-tabs-panel">Totals for the selected period.</div>
<div class="wu-tabs-panel" hidden>One row per order.</div>
<div class="wu-tabs-panel" hidden>Download as CSV.</div>
</div>

When the tabs do not fit, the list scrolls sideways and fades out at the edge that has more tabs behind it.

Status, owner and due date.
<div class="wu-tabs" style="max-inline-size: 20rem">
<div class="wu-tabs-list" aria-label="Project">
<button class="wu-tabs-tab" type="button" aria-selected="true">Overview</button>
<button class="wu-tabs-tab" type="button">Tasks</button>
<button class="wu-tabs-tab" type="button">Files</button>
<button class="wu-tabs-tab" type="button">Members</button>
<button class="wu-tabs-tab" type="button">Activity</button>
<button class="wu-tabs-tab" type="button">Settings</button>
</div>
<div class="wu-tabs-panel">Status, owner and due date.</div>
<div class="wu-tabs-panel" hidden>Open and done tasks.</div>
<div class="wu-tabs-panel" hidden>Shared documents.</div>
<div class="wu-tabs-panel" hidden>People on the project.</div>
<div class="wu-tabs-panel" hidden>Recent changes.</div>
<div class="wu-tabs-panel" hidden>Name and visibility.</div>
</div>
Three items, $1,240.00.
Three items, $1,240.00.
<div class="flex col gap-6">
<div class="wu-tabs card">
<div class="wu-tabs-list" aria-label="Order, card">
<button class="wu-tabs-tab" type="button" aria-selected="true">Items</button>
<button class="wu-tabs-tab" type="button">Shipping</button>
<button class="wu-tabs-tab" type="button">History</button>
</div>
<div class="wu-tabs-panel">Three items, $1,240.00.</div>
<div class="wu-tabs-panel" hidden>Ships from Dallas.</div>
<div class="wu-tabs-panel" hidden>Placed May 4, paid May 4.</div>
</div>
<div class="wu-tabs boxed">
<div class="wu-tabs-list" aria-label="Order, boxed">
<button class="wu-tabs-tab" type="button" aria-selected="true">Items</button>
<button class="wu-tabs-tab" type="button">Shipping</button>
<button class="wu-tabs-tab" type="button">History</button>
</div>
<div class="wu-tabs-panel">Three items, $1,240.00.</div>
<div class="wu-tabs-panel" hidden>Ships from Dallas.</div>
<div class="wu-tabs-panel" hidden>Placed May 4, paid May 4.</div>
</div>
</div>

vertical puts the tab list at the inline start with the selected bar on its inline end edge. The script sets aria-orientation="vertical" on the list, and Up and Down arrows move between tabs. Without the script, add aria-orientation="vertical" yourself.

Name, language and time zone.
<div class="wu-tabs vertical">
<div class="wu-tabs-list" aria-label="Settings" aria-orientation="vertical">
<button class="wu-tabs-tab" type="button" aria-selected="true">General</button>
<button class="wu-tabs-tab" type="button">Notifications</button>
<button class="wu-tabs-tab" type="button">Security</button>
</div>
<div class="wu-tabs-panel">Name, language and time zone.</div>
<div class="wu-tabs-panel" hidden>Email and push alerts.</div>
<div class="wu-tabs-panel" hidden>Password and two-step sign-in.</div>
</div>

An <svg> placed in a tab is sized to the text and takes the text color. Bring your own icons.

Twelve open threads.
<div class="wu-tabs">
<div class="wu-tabs-list" aria-label="Inbox">
<button class="wu-tabs-tab" type="button" aria-selected="true"><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6" fill="currentColor"/></svg>Open</button>
<button class="wu-tabs-tab" type="button"><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="2"/></svg>Closed</button>
</div>
<div class="wu-tabs-panel">Twelve open threads.</div>
<div class="wu-tabs-panel" hidden>Forty closed threads.</div>
</div>

The list starts at the right, the fade and the arrow keys follow the reading direction, and vertical tabs sit on the right.

שם וכתובת אימייל.
<div class="wu-tabs vertical">
<div class="wu-tabs-list" aria-label="הפרופיל" aria-orientation="vertical">
<button class="wu-tabs-tab" type="button" aria-selected="true">חשבון</button>
<button class="wu-tabs-tab" type="button">פרטיות</button>
</div>
<div class="wu-tabs-panel">שם וכתובת אימייל.</div>
<div class="wu-tabs-panel" hidden>מי יכול לראות את הפרופיל שלך.</div>
</div>

The animated word grows the selected bar from the center with a spring and fades each panel in. wu-animated on an ancestor turns it on for every tab set inside. Motion runs only under prefers-reduced-motion: no-preference.

42 orders this week.
<div class="wu-tabs animated">
<div class="wu-tabs-list" aria-label="Period">
<button class="wu-tabs-tab" type="button" aria-selected="true">Week</button>
<button class="wu-tabs-tab" type="button">Month</button>
<button class="wu-tabs-tab" type="button">Year</button>
</div>
<div class="wu-tabs-panel">42 orders this week.</div>
<div class="wu-tabs-panel" hidden>180 orders this month.</div>
<div class="wu-tabs-panel" hidden>2,140 orders this year.</div>
</div>

The drop-in script and the ES module both start every wu-tabs on the page. For tabs added later, call init() on their container:

import { init } from '@wovenui/components/js';
init(document.getElementById('settings'));

Each tab set starts once, so calling init() again is safe. See JavaScript.

  • The script follows the WAI-ARIA tabs pattern with automatic activation: Left and Right arrows select the previous or next tab, Home and End the first and last, mirrored in right-to-left text. In vertical tabs, Up and Down arrows do it instead. Only the selected tab is in the Tab order.
  • Panels get tabindex="0", so Tab moves from the tab list into the panel. Focus shows a ring on tabs and panels.
  • Give the tab list an aria-label or aria-labelledby.
  • In forced colors mode the selected bar uses the highlight color.
@use "@wovenui/components/tabs";

The tabs need no other component.