Tabs
stimeo--tabs
Tabbed panels. Moving with the arrow keys selects the tab you land on.
Implements the WAI-ARIA Tabs pattern. Moving through the tabs with the arrow keys selects the one you land on. Tab stops on the tab strip once, so it never gets in the way of walking the page from the keyboard. Which tab is selected is announced to screen readers, and only its panel is shown. Tabs pair to panels by their order.
Keyboard
| Key | Action |
|---|---|
| → / ← | Select the next / previous tab (wrapping). Under RTL the two swap. |
| Home / End | Select the first / last tab. |
<%# Markup for the tabs (APG Tabs / automatic activation) demo.
stimeo--tabs provides aria-selected, roving tabindex, arrow-key switching, and panel
visibility control. The initially selected tab has aria-selected="true" and
non-selected panels carry hidden (to prevent SSR flicker). %>
<div class="tabs" data-controller="stimeo--tabs">
<div class="tabs__list" role="tablist" aria-label="<%= t("components.tabs.demo.label") %>"
data-stimeo--tabs-target="list">
<button class="tabs__tab" role="tab" id="tab-overview" aria-controls="panel-overview"
aria-selected="true"
data-stimeo--tabs-target="tab"
data-action="click->stimeo--tabs#select
keydown->stimeo--tabs#onKeydown"><%= t(
"components.tabs.demo.overview"
) %></button>
<button class="tabs__tab" role="tab" id="tab-profile" aria-controls="panel-profile"
aria-selected="false" tabindex="-1"
data-stimeo--tabs-target="tab"
data-action="click->stimeo--tabs#select
keydown->stimeo--tabs#onKeydown"><%= t(
"components.tabs.demo.profile"
) %></button>
<button class="tabs__tab" role="tab" id="tab-billing" aria-controls="panel-billing"
aria-selected="false" tabindex="-1"
data-stimeo--tabs-target="tab"
data-action="click->stimeo--tabs#select
keydown->stimeo--tabs#onKeydown"><%= t(
"components.tabs.demo.billing"
) %></button>
</div>
<div class="tabs__panel" role="tabpanel" id="panel-overview" aria-labelledby="tab-overview"
data-stimeo--tabs-target="panel"><%= t("components.tabs.demo.overview_content") %></div>
<div class="tabs__panel" role="tabpanel" id="panel-profile" aria-labelledby="tab-profile"
data-stimeo--tabs-target="panel" hidden><%= t(
"components.tabs.demo.profile_content"
) %></div>
<div class="tabs__panel" role="tabpanel" id="panel-billing" aria-labelledby="tab-billing"
data-stimeo--tabs-target="panel" hidden><%= t(
"components.tabs.demo.billing_content"
) %></div>
</div>
/*
* Presentation-only styles for the tabs demo.
* The selected state is expressed via aria-selected (set by the library).
*/
.tabs__list {
display: flex;
gap: 0.25rem;
border-bottom: 1px solid var(--border);
}
.tabs__tab {
padding: 0.5rem 1rem;
font-size: 0.95rem;
cursor: pointer;
color: var(--muted);
background: transparent;
border: 0;
border-bottom: 2px solid transparent;
}
.tabs__tab[aria-selected="true"] {
color: var(--fg);
border-bottom-color: var(--accent);
}
.tabs__panel {
padding: 1rem 0.25rem;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--tabs"
Targets
| Name | Description | Attribute |
|---|---|---|
tab
required
|
A tab button (role=tab) paired to the panel of the same index; carries aria-selected and roving tabindex. |
data-stimeo--tabs-target="tab" |
panel
required
|
A tab panel (role=tabpanel); its hidden attribute reflects whether its tab is selected. |
data-stimeo--tabs-target="panel" |
list
required
|
data-stimeo--tabs-target="list" |
Actions
| Name | Description | Action |
|---|---|---|
onKeydown
|
Arrow/Home/End navigation with automatic activation: focusing a tab selects it. | stimeo--tabs#onKeydown |
select
|
Selects the clicked tab, updating aria-selected, tabindex, and panel visibility. |
stimeo--tabs#select |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-selected |
Each tab | "true" on the active tab, "false" on the others. You can render the initial tab server-side; if several are marked, the first wins. |
tabindex |
Each tab | 0 on the active tab, -1 on the others (roving tabindex). |
hidden |
Each panel | Only the selected tab's panel is shown. |