Tabs
An accessible tab interface with roving tabindex, arrow-key navigation, and horizontal or vertical orientation. Tab labels accept plain strings or snippets.
Import
import { Tabs } from "@hyzer-labs/ui"Demo
A stable distance driver for windy rounds: predictable fade, without fighting you on hyzer lines.
Speed 12 · Glide 5 · Turn −1 · Fade 3. Best for arm speeds above 350 ft.
"Handles headwinds beautifully. My new go-to off the tee." · 4.8/5 from 212 rounds
Comparison view is coming soon.
<!-- The panel snippet receives the item — render per-item content -->
<Tabs items={discTabs} ariaLabel="Disc details">
{#snippet panel(item)}
<p>{discContent[item.id]}</p>
{/snippet}
</Tabs>A stable distance driver for windy rounds: predictable fade, without fighting you on hyzer lines.
Speed 12 · Glide 5 · Turn −1 · Fade 3. Best for arm speeds above 350 ft.
"Handles headwinds beautifully. My new go-to off the tee." · 4.8/5 from 212 rounds
Comparison view is coming soon.
<!-- The panel snippet receives the item — render per-item content -->
<Tabs items={discTabs} orientation="vertical" ariaLabel="Disc details">
{#snippet panel(item)}
<p>{discContent[item.id]}</p>
{/snippet}
</Tabs>With auto (default), arrow keys activate tabs as focus moves. With manual, arrows only move focus, and Enter or Space activates. Try arrowing through both with the keyboard.
A stable distance driver for windy rounds: predictable fade, without fighting you on hyzer lines.
Speed 12 · Glide 5 · Turn −1 · Fade 3. Best for arm speeds above 350 ft.
"Handles headwinds beautifully. My new go-to off the tee." · 4.8/5 from 212 rounds
Comparison view is coming soon.
<Tabs items={discTabs} ariaLabel="Disc details">
{#snippet panel(item)}
<p>{discContent[item.id]}</p>
{/snippet}
</Tabs>A stable distance driver for windy rounds: predictable fade, without fighting you on hyzer lines.
Speed 12 · Glide 5 · Turn −1 · Fade 3. Best for arm speeds above 350 ft.
"Handles headwinds beautifully. My new go-to off the tee." · 4.8/5 from 212 rounds
Comparison view is coming soon.
<Tabs items={discTabs} activation="manual" ariaLabel="Disc details">
{#snippet panel(item)}
<p>{discContent[item.id]}</p>
{/snippet}
</Tabs>A stable distance driver for windy rounds: predictable fade, without fighting you on hyzer lines.
"Handles headwinds beautifully. My new go-to off the tee." · 4.8/5 from 212 rounds
<!-- label accepts string | Snippet — use a snippet for inner markup -->
{#snippet reviewsLabel()}
Reviews <span class="badge">212</span>
{/snippet}
<Tabs items={[{ id: 'overview', label: 'Overview' }, { id: 'reviews', label: reviewsLabel }]} ariaLabel="Disc details">
{#snippet panel(item)}…{/snippet}
</Tabs>Active: overview
A stable distance driver for windy rounds: predictable fade, without fighting you on hyzer lines.
Speed 12 · Glide 5 · Turn −1 · Fade 3. Best for arm speeds above 350 ft.
"Handles headwinds beautifully. My new go-to off the tee." · 4.8/5 from 212 rounds
Comparison view is coming soon.
<Tabs items={discTabs} onChange={(id) => (activeId = id)} ariaLabel="Disc details">
{#snippet panel(item)}…{/snippet}
</Tabs>Props
| Name | Type | Default | Note |
|---|---|---|---|
items | TabItem[] | — | Required. See TabItem below. |
ariaLabel | string | — | Required. Labels the tablist. |
defaultTab | string | — | ID of the initially active tab. |
orientation | 'horizontal' | 'vertical' | 'horizontal' | |
activation | 'auto' | 'manual' | 'auto' | auto activates on arrow-key focus; manual waits for Enter/Space. |
panel | Snippet<[TabItem]> | — | Required. Renders each panel. |
onChange | (activeId: string) => void | — | |
class | string | — | Merged after the hz-tabs class. |
TabItem
| Name | Type | Default | Note |
|---|---|---|---|
id | string | — | Required. Must be unique. |
label | string | Snippet | — | Required. String for plain text; snippet for inner markup. |
disabled | boolean | false |
Theme hooks
What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.
Root class: .hz-tabs
Data attributes
| Hook | Values | Styles |
|---|---|---|
data-orientation | 'horizontal' | 'vertical' | Vertical moves the list border and the active indicator to the inline edge. Default horizontal. |
data-state | 'active' | 'inactive' | On .hz-tabs-trigger. The panel carries it too, but its visibility runs off the native hidden attribute. |
data-disabled | present on disabled tabs | On .hz-tabs-trigger. Disabled tabs stay focusable (aria-disabled, not the native attribute). |
Part classes
| Hook | Values | Styles |
|---|---|---|
.hz-tabs-list | child element | The tablist. Scrolls horizontally. |
.hz-tabs-trigger | child element | One tab. The theme owns the whole button reset here, so an unthemed Tabs renders native buttons. The active indicator is an inset box-shadow, deliberately: the list scrolls, and any overlap trick that adds layout size produces a phantom scrollbar. Keep that property if you restyle it. |
.hz-tabs-panel | child element | One panel. |
Accessibility
Implements the WAI-ARIA tabs pattern: role='tablist'/'tab'/'tabpanel', aria-selected, and roving tabindex. Tab is one stop for the whole tablist, and arrow keys move between triggers, with Home/End jumping to the first/last.
Panels are only their own tab stop when they contain no focusable content. ariaLabel is required to name the tablist.
Disabled tabs stay focusable via arrows but carry aria-disabled='true' and never activate.
References: APG Tabs pattern