Tabs
Stable Available in release assets
Organize related sections with an authored ARIA tablist and an optional, dependency-free keyboard and panel coordinator.
Tabs primary example
<section class="thin-stack">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product details">
<button id="details-tab" role="tab" aria-controls="details-panel" aria-selected="true">Details</button>
<button id="shipping-tab" role="tab" aria-controls="shipping-panel" aria-selected="false">Shipping and returns</button>
<button role="tab" aria-disabled="true">Unavailable</button>
</div>
<section id="details-panel" role="tabpanel" aria-labelledby="details-tab">Product details</section>
<section id="shipping-panel" role="tabpanel" aria-labelledby="shipping-tab">Shipping and returns</section>
</section>Show code
<section class="thin-stack">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product details">
<button id="details-tab" role="tab" aria-controls="details-panel" aria-selected="true">Details</button>
<button id="shipping-tab" role="tab" aria-controls="shipping-panel" aria-selected="false">Shipping and returns</button>
<button role="tab" aria-disabled="true">Unavailable</button>
</div>
<section id="details-panel" role="tabpanel" aria-labelledby="details-tab">Product details</section>
<section id="shipping-panel" role="tabpanel" aria-labelledby="shipping-tab">Shipping and returns</section>
</section>Variant gallery
Each authored example uses the production assets and includes copyable markup.
Horizontal automatic tabs
Focus the selected tab, then press Left or Right to move and select. Home and End move to the available bounds; the native disabled tab is skipped. Without JavaScript, the links lead to panels that remain readable.
<section style="min-inline-size: 0; overflow-wrap: anywhere; ">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product details" >
<a id="tabs-horizontal-overview-tab" role="tab" href="#tabs-horizontal-overview" aria-controls="tabs-horizontal-overview" aria-selected="true">Overview</a>
<a id="tabs-horizontal-delivery-tab" role="tab" href="#tabs-horizontal-delivery" aria-controls="tabs-horizontal-delivery" aria-selected="false">Delivery</a>
<button id="tabs-horizontal-unavailable-tab" type="button" role="tab" aria-controls="tabs-horizontal-unavailable" aria-selected="false" disabled>Unavailable</button>
</div>
<section id="tabs-horizontal-overview" role="tabpanel" aria-labelledby="tabs-horizontal-overview-tab">
<p>Product information remains ordinary authored content.</p>
</section>
<section id="tabs-horizontal-delivery" role="tabpanel" aria-labelledby="tabs-horizontal-delivery-tab">
<p>Delivery takes three to five business days. <a href="/delivery/">Read the delivery policy</a>.</p>
</section>
<section id="tabs-horizontal-unavailable" role="tabpanel" aria-labelledby="tabs-horizontal-unavailable-tab">
<p>This service is currently unavailable.</p>
</section>
</section>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<section style="min-inline-size: 0; overflow-wrap: anywhere; ">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product details" >
<a id="tabs-horizontal-overview-tab" role="tab" href="#tabs-horizontal-overview" aria-controls="tabs-horizontal-overview" aria-selected="true">Overview</a>
<a id="tabs-horizontal-delivery-tab" role="tab" href="#tabs-horizontal-delivery" aria-controls="tabs-horizontal-delivery" aria-selected="false">Delivery</a>
<button id="tabs-horizontal-unavailable-tab" type="button" role="tab" aria-controls="tabs-horizontal-unavailable" aria-selected="false" disabled>Unavailable</button>
</div>
<section id="tabs-horizontal-overview" role="tabpanel" aria-labelledby="tabs-horizontal-overview-tab">
<p>Product information remains ordinary authored content.</p>
</section>
<section id="tabs-horizontal-delivery" role="tabpanel" aria-labelledby="tabs-horizontal-delivery-tab">
<p>Delivery takes three to five business days. <a href="/delivery/">Read the delivery policy</a>.</p>
</section>
<section id="tabs-horizontal-unavailable" role="tabpanel" aria-labelledby="tabs-horizontal-unavailable-tab">
<p>This service is currently unavailable.</p>
</section>
</section>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Vertical automatic tabs
Up and Down move and select in this authored vertical tablist. All panels are present before enhancement.
<section style="min-inline-size: 0; overflow-wrap: anywhere; ">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product details" aria-orientation="vertical">
<a id="tabs-vertical-overview-tab" role="tab" href="#tabs-vertical-overview" aria-controls="tabs-vertical-overview" aria-selected="true">Overview</a>
<a id="tabs-vertical-delivery-tab" role="tab" href="#tabs-vertical-delivery" aria-controls="tabs-vertical-delivery" aria-selected="false">Delivery</a>
<button id="tabs-vertical-unavailable-tab" type="button" role="tab" aria-controls="tabs-vertical-unavailable" aria-selected="false" disabled>Unavailable</button>
</div>
<section id="tabs-vertical-overview" role="tabpanel" aria-labelledby="tabs-vertical-overview-tab">
<p>Product information remains ordinary authored content.</p>
</section>
<section id="tabs-vertical-delivery" role="tabpanel" aria-labelledby="tabs-vertical-delivery-tab">
<p>Delivery takes three to five business days. <a href="/delivery/">Read the delivery policy</a>.</p>
</section>
<section id="tabs-vertical-unavailable" role="tabpanel" aria-labelledby="tabs-vertical-unavailable-tab">
<p>This service is currently unavailable.</p>
</section>
</section>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<section style="min-inline-size: 0; overflow-wrap: anywhere; ">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product details" aria-orientation="vertical">
<a id="tabs-vertical-overview-tab" role="tab" href="#tabs-vertical-overview" aria-controls="tabs-vertical-overview" aria-selected="true">Overview</a>
<a id="tabs-vertical-delivery-tab" role="tab" href="#tabs-vertical-delivery" aria-controls="tabs-vertical-delivery" aria-selected="false">Delivery</a>
<button id="tabs-vertical-unavailable-tab" type="button" role="tab" aria-controls="tabs-vertical-unavailable" aria-selected="false" disabled>Unavailable</button>
</div>
<section id="tabs-vertical-overview" role="tabpanel" aria-labelledby="tabs-vertical-overview-tab">
<p>Product information remains ordinary authored content.</p>
</section>
<section id="tabs-vertical-delivery" role="tabpanel" aria-labelledby="tabs-vertical-delivery-tab">
<p>Delivery takes three to five business days. <a href="/delivery/">Read the delivery policy</a>.</p>
</section>
<section id="tabs-vertical-unavailable" role="tabpanel" aria-labelledby="tabs-vertical-unavailable-tab">
<p>This service is currently unavailable.</p>
</section>
</section>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Manual activation
Left and Right move focus without selecting. Press Enter or Space to select the focused tab. Each example starts the optional coordinator from the declared behaviors.js asset.
<section style="min-inline-size: 0; overflow-wrap: anywhere; ">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product details" data-activation="manual">
<a id="tabs-manual-overview-tab" role="tab" href="#tabs-manual-overview" aria-controls="tabs-manual-overview" aria-selected="true">Overview</a>
<a id="tabs-manual-delivery-tab" role="tab" href="#tabs-manual-delivery" aria-controls="tabs-manual-delivery" aria-selected="false">Delivery</a>
<button id="tabs-manual-unavailable-tab" type="button" role="tab" aria-controls="tabs-manual-unavailable" aria-selected="false" disabled>Unavailable</button>
</div>
<section id="tabs-manual-overview" role="tabpanel" aria-labelledby="tabs-manual-overview-tab">
<p>Product information remains ordinary authored content.</p>
</section>
<section id="tabs-manual-delivery" role="tabpanel" aria-labelledby="tabs-manual-delivery-tab">
<p>Delivery takes three to five business days. <a href="/delivery/">Read the delivery policy</a>.</p>
</section>
<section id="tabs-manual-unavailable" role="tabpanel" aria-labelledby="tabs-manual-unavailable-tab">
<p>This service is currently unavailable.</p>
</section>
</section>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<section style="min-inline-size: 0; overflow-wrap: anywhere; ">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product details" data-activation="manual">
<a id="tabs-manual-overview-tab" role="tab" href="#tabs-manual-overview" aria-controls="tabs-manual-overview" aria-selected="true">Overview</a>
<a id="tabs-manual-delivery-tab" role="tab" href="#tabs-manual-delivery" aria-controls="tabs-manual-delivery" aria-selected="false">Delivery</a>
<button id="tabs-manual-unavailable-tab" type="button" role="tab" aria-controls="tabs-manual-unavailable" aria-selected="false" disabled>Unavailable</button>
</div>
<section id="tabs-manual-overview" role="tabpanel" aria-labelledby="tabs-manual-overview-tab">
<p>Product information remains ordinary authored content.</p>
</section>
<section id="tabs-manual-delivery" role="tabpanel" aria-labelledby="tabs-manual-delivery-tab">
<p>Delivery takes three to five business days. <a href="/delivery/">Read the delivery policy</a>.</p>
</section>
<section id="tabs-manual-unavailable" role="tabpanel" aria-labelledby="tabs-manual-unavailable-tab">
<p>This service is currently unavailable.</p>
</section>
</section>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Anatomy
- Labelled tablist
- Native button or link tabs
- Panels referenced by aria-controls
Variants and sizes
- Horizontal — shown above
- Vertical — shown above
- Automatic activation — shown above
- Manual activation — shown above
States and difficult content
Rendered fixtures exercise authored content and constrained layouts with the same production assets.
Selected, unselected and disabled tabs
The first tab starts selected, the second can be selected, and the native disabled third tab cannot activate. With no JavaScript all three panel descriptions stay in document order.
<section style="min-inline-size: 0; overflow-wrap: anywhere; ">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product details" >
<a id="tabs-states-overview-tab" role="tab" href="#tabs-states-overview" aria-controls="tabs-states-overview" aria-selected="true">Overview</a>
<a id="tabs-states-delivery-tab" role="tab" href="#tabs-states-delivery" aria-controls="tabs-states-delivery" aria-selected="false">Delivery</a>
<button id="tabs-states-unavailable-tab" type="button" role="tab" aria-controls="tabs-states-unavailable" aria-selected="false" disabled>Unavailable</button>
</div>
<section id="tabs-states-overview" role="tabpanel" aria-labelledby="tabs-states-overview-tab">
<p>Product information remains ordinary authored content.</p>
</section>
<section id="tabs-states-delivery" role="tabpanel" aria-labelledby="tabs-states-delivery-tab">
<p>Delivery takes three to five business days. <a href="/delivery/">Read the delivery policy</a>.</p>
</section>
<section id="tabs-states-unavailable" role="tabpanel" aria-labelledby="tabs-states-unavailable-tab">
<p>This service is currently unavailable.</p>
</section>
</section>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<section style="min-inline-size: 0; overflow-wrap: anywhere; ">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Product details" >
<a id="tabs-states-overview-tab" role="tab" href="#tabs-states-overview" aria-controls="tabs-states-overview" aria-selected="true">Overview</a>
<a id="tabs-states-delivery-tab" role="tab" href="#tabs-states-delivery" aria-controls="tabs-states-delivery" aria-selected="false">Delivery</a>
<button id="tabs-states-unavailable-tab" type="button" role="tab" aria-controls="tabs-states-unavailable" aria-selected="false" disabled>Unavailable</button>
</div>
<section id="tabs-states-overview" role="tabpanel" aria-labelledby="tabs-states-overview-tab">
<p>Product information remains ordinary authored content.</p>
</section>
<section id="tabs-states-delivery" role="tabpanel" aria-labelledby="tabs-states-delivery-tab">
<p>Delivery takes three to five business days. <a href="/delivery/">Read the delivery policy</a>.</p>
</section>
<section id="tabs-states-unavailable" role="tabpanel" aria-labelledby="tabs-states-unavailable-tab">
<p>This service is currently unavailable.</p>
</section>
</section>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Long labels in a narrow tablist
This constrained tablist scrolls horizontally rather than clipping destinations or widening the page. Use arrow keys to reach the next tab; focus scrolls it into view. The panels wrap independently below it.
<section style="min-inline-size: 0; overflow-wrap: anywhere; max-inline-size: 18rem" lang="de">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Produktdetails" >
<a id="tabs-long-overview-tab" role="tab" href="#tabs-long-overview" aria-controls="tabs-long-overview" aria-selected="true">Produktinformationen und ausführliche technische Eigenschaften</a>
<a id="tabs-long-delivery-tab" role="tab" href="#tabs-long-delivery" aria-controls="tabs-long-delivery" aria-selected="false">Internationale Lieferung und Rücksendebedingungen</a>
<button id="tabs-long-unavailable-tab" type="button" role="tab" aria-controls="tabs-long-unavailable" aria-selected="false" disabled>Unavailable</button>
</div>
<section id="tabs-long-overview" role="tabpanel" aria-labelledby="tabs-long-overview-tab">
<p>Product information remains ordinary authored content.</p>
</section>
<section id="tabs-long-delivery" role="tabpanel" aria-labelledby="tabs-long-delivery-tab">
<p>Delivery takes three to five business days. <a href="/delivery/">Read the delivery policy</a>.</p>
</section>
<section id="tabs-long-unavailable" role="tabpanel" aria-labelledby="tabs-long-unavailable-tab">
<p>This service is currently unavailable.</p>
</section>
</section>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>Show code
<section style="min-inline-size: 0; overflow-wrap: anywhere; max-inline-size: 18rem" lang="de">
<div class="thin-tabs" data-enhance="tabs" role="tablist" aria-label="Produktdetails" >
<a id="tabs-long-overview-tab" role="tab" href="#tabs-long-overview" aria-controls="tabs-long-overview" aria-selected="true">Produktinformationen und ausführliche technische Eigenschaften</a>
<a id="tabs-long-delivery-tab" role="tab" href="#tabs-long-delivery" aria-controls="tabs-long-delivery" aria-selected="false">Internationale Lieferung und Rücksendebedingungen</a>
<button id="tabs-long-unavailable-tab" type="button" role="tab" aria-controls="tabs-long-unavailable" aria-selected="false" disabled>Unavailable</button>
</div>
<section id="tabs-long-overview" role="tabpanel" aria-labelledby="tabs-long-overview-tab">
<p>Product information remains ordinary authored content.</p>
</section>
<section id="tabs-long-delivery" role="tabpanel" aria-labelledby="tabs-long-delivery-tab">
<p>Delivery takes three to five business days. <a href="/delivery/">Read the delivery policy</a>.</p>
</section>
<section id="tabs-long-unavailable" role="tabpanel" aria-labelledby="tabs-long-unavailable-tab">
<p>This service is currently unavailable.</p>
</section>
</section>
<script type="module">
import { observeNativeBehaviors } from "/behaviors.js";
observeNativeBehaviors();
</script>- Selected — shown above
- Unselected — shown above
- Disabled — shown above
- Long labels — shown above
- Overflowing narrow container — shown above
Interaction and keyboard behavior
Arrow Right/Left moves through horizontal tabs; Arrow Down/Up moves through vertical tabs; Home and End move to the bounds. Automatic activation selects on focus, while data-activation=manual waits for Enter or Space. Disabled tabs are skipped.
Accessibility
Give the tablist an accessible label, use role=tab with aria-controls and aria-selected, and give every panel role=tabpanel with aria-labelledby. Keep all panel content in authored HTML so it remains available before enhancement.
- Contract
TABS· APG pattern- Basis
- APG Tabs with deliberate roving tabindex and automatic/manual activation
- Behavior guarantees
- Applies the APG Tabs relationships only when each enabled tab controls exactly one panel; unresolved or duplicate targets fail closed so no authored panel is hidden.
- Synchronizes aria-selected, panel visibility, tabpanel, and aria-labelledby, and restores a surviving tab's focus through refresh.
- Reconciles tablists after child-list morphs.
- Evidence
tests/unit/behaviors/tabs.test.tstests/unit/library/advanced-component-contracts.test.ts
API
Use .thin-tabs on a labelled tablist. Add data-enhance=tabs and call observeNativeBehaviors() from behaviors.js to coordinate the tabs. Set aria-orientation=vertical or data-activation=manual when needed. Replace the authored tabs through a normal DOM update to refresh the behavior.
Behavior tier: dependency-free
Structured host, property, and example tables are not authored for this item yet. Use the summary above and the primary example.
Fallback
Without JavaScript, the authored controls and all ordinary panel content remain in document order; links may be used when navigation is the canonical interaction.
Assets and dependencies
/components.css/behaviors.js
Dependencies: None
Source: packages/ui/src/items/tabs/style.ts
Stability evidence
- Horizontal, vertical, disabled, manual, automatic, and morph-safe keyboard behavior:
test:tests/unit/behaviors/tabs.test.ts - Native host reconnect contract:
test:tests/browser/cdn-starter.spec.ts - Cross-theme, density, narrow-container, and long-label review fixture:
review:site/src/pages/quality/index.astro#quality-review
Composition
Derived from the examples and style specs, not authored.