HyTabNav
Usage examples on this page are written for Lit / plain HTML (
<hy-tab-nav>). The same component ships asHyTabNavin@whitespaceux/harmony-react(native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.
Child components: <hy-tab-nav-item> — used only inside this one.
Link navigation that looks like tabs: a <nav> landmark of hy-tab-nav-item links, one URL each.
Tab nav, tab group or toggle group?
Pick by what changes, never by look. Each item is its own page and the URL changes: a tab nav. Panels switch in place: hy-tab-group. A value is picked — a view mode (list, map, grid), a density, a unit: hy-toggle-group.
Use it for secondary navigation between pages (a settings section switcher, a top strip that changes the route): the back button works, each item is a link, and the current item carries aria-current="page". hy-tab-group is the in-place counterpart (role="tablist", panels switch without a URL change); hy-toggle-group is a value picker. Pick by one question — does the URL change?
Tabs vs tab nav vs toggle group
| In place (no URL) | Navigates (URL) | |
|---|---|---|
| Tab-shaped | hy-tab-group / hy-tab | hy-tab-nav / hy-tab-nav-item |
| Value picker | hy-toggle-group | — |
Industry precedent for keeping the two lookalikes apart: Radix Themes Tabs vs TabNav, Primer UnderlinePanels vs UnderlineNav, GOV.UK secondary navigation. There is no role="tablist", no aria-selected and no roving tabindex here — these are links, and Tab moves between them.
What it syncs
emphasis, size and orientation are pushed down to every child item on first render, on prop change and whenever the slotted items change, exactly like hy-tab-group does for its tabs. The chrome is pixel-identical to hy-tab at every rung, so a tab strip and a link strip sitting side by side read as one family.
Narrow regions
By default the items stay on one row, exactly like hy-tab-group. In a region that can get narrow (a resizable rail), set wrap: the item list wraps onto further rows while the action regions stay at the trailing edge, centered on the whole list block, and never shrink — the collapse control is the last thing to go, not the first. Links wrap; tabs don't (GOV.UK service navigation, Bootstrap pills).
Actions
The actions-start / actions-end slots host controls that apply to the whole strip (a collapse button, a global search). They render beside the item list — never inside it — and each region becomes a labeled role="group" when it holds 2 or more controls; name it with actions-start-label / actions-end-label. Its controls are reached with Tab (not a role="toolbar", which would promise arrow keys between them). For one tab stop with arrow keys, slot an hy-toolbar into the region instead.
When to use
- Sibling pages under one section, each with its own address: a project's Overview, Tasks and Files.
Do
- Keep tab labels short and parallel.
- Mark the current page with
active.
When not to use
- The app's main destinations:
hy-sidebarorhy-topbar. A tab nav is the second tier under them. - A full navigation rail down the side of the app:
hy-sidebar. Keeporientation="vertical"for a short column of sibling pages. - Only one destination: an
hy-link, not a strip.
Accessibility: your part
- Keep
hrefon everyhy-tab-nav-item, also when a client-side router takes over throughactivate, so each item stays a real link.
Examples
Section switcher (soft pills)
<hy-tab-nav label="Tokens section" emphasis="soft" size="small">
<hy-tab-nav-item label="Primitives" href="/tokens/primitives" active></hy-tab-nav-item>
<hy-tab-nav-item label="Semantics" href="/tokens/semantics"></hy-tab-nav-item>
<hy-tab-nav-item label="Components" href="/tokens/components"></hy-tab-nav-item>
<hy-icon-button slot="actions-end" name="arrow-collapse" label="Hide navigation"></hy-icon-button>
</hy-tab-nav>Underline strip (the default plain emphasis)
<hy-tab-nav label="Project">
<hy-tab-nav-item label="Overview" href="/overview" active></hy-tab-nav-item>
<hy-tab-nav-item label="Activity" href="/activity"></hy-tab-nav-item>
<hy-tab-nav-item label="Settings" href="/settings"></hy-tab-nav-item>
</hy-tab-nav>Vertical
<hy-tab-nav label="Account" orientation="vertical">
<hy-tab-nav-item label="Profile" href="/account/profile" active></hy-tab-nav-item>
<hy-tab-nav-item label="Security" href="/account/security"></hy-tab-nav-item>
<hy-tab-nav-item label="Billing" href="/account/billing"></hy-tab-nav-item>
</hy-tab-nav>Client-side routing
<hy-tab-nav label="Project" id="project-nav">
<hy-tab-nav-item label="Overview" value="overview" href="/overview" active></hy-tab-nav-item>
<hy-tab-nav-item label="Activity" value="activity" href="/activity"></hy-tab-nav-item>
</hy-tab-nav>
<script>
document.getElementById('project-nav').addEventListener('activate', (e) => {
e.detail.originalEvent.preventDefault(); // stop the full-page navigation
router.push(e.detail.href);
});
</script>API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
label | label | string | '' | Accessible name of the <nav> landmark — required. A page usually has several navigation landmarks, and the name is how a screen-reader user tells them apart. Dev mode warns when it is empty. |
orientation | orientation | 'horizontal' | 'vertical' | 'horizontal' | Layout axis propagated to every child item. - horizontal (default) — a row, indicator on the block-end edge - vertical — a column, indicator on the inline-start edge |
emphasis | emphasis | 'solid' | 'soft' | 'outlined' | 'plain' | 'plain' | Visual chrome weight propagated to every child item. - plain (default) — transparent rest, role underline indicator - soft — role-tinted rounded rectangle on the current item - outlined — boxed/bordered items - solid — fully filled current item |
size | size | 'small' | 'medium' | 'large' | 'medium' | Type + padding rung propagated to every child item. - small — label-md type on row-nav-md padding - medium (default) — body-md type on row-nav-lg padding - large — body-lg type on row-nav-lg padding |
wrap | wrap | boolean | false | Lets the item list wrap onto further rows when the region is too narrow for one row. The action regions stay at the trailing edge, centered on the list block, and never shrink. Off by default so the strip stays a pixel lookalike of hy-tab-group, which keeps one row. |
actionsStartLabel | actions-start-label | string | '' | Accessible name applied as aria-label to the leading actions region when it becomes a labeled role="group" (2+ controls). |
actionsEndLabel | actions-end-label | string | '' | Accessible name for the trailing actions region when it becomes a labeled role="group". |
Slots
| Slot | Description |
|---|---|
default | The hy-tab-nav-item elements. |
actions-start | Strip-wide controls rendered at the leading edge, beside the item list. Becomes a labeled role="group" with 2+ controls. |
actions-end | Strip-wide controls rendered at the trailing edge. Becomes a labeled role="group" with 2+ controls. |
CSS Parts
| Part | Description |
|---|---|
base | The <nav> landmark. |
list | The container wrapping the items. |
actions-start | The leading actions region. |
actions-end | The trailing actions region. |
CSS Custom Properties
The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.
| Property | Default | Description |
|---|---|---|
--hy-tab-nav-gap-layout-2xs | var(--hy-gap-layout-2xs) | Gap between items. In a row the items touch (as in hy-tab-group); in a column they sit var(--hy-gap-layout-2xs) apart. |
--hy-tab-nav-gap-layout-sm | var(--hy-gap-layout-sm) | Gap between the item list and the action regions, and between controls within a region. |