Skip to content
Home Theme Gallery

Tabs

The custom element is <hui-tabs>.

Overview

A single tab stop with roving tabindex, arrow movement, Home and End, and the choice of automatic or manual activation. Tabs and panels are light-DOM children the Go host rendered, so HTMX can swap panel content in place. With navigation it becomes a landmark of real links whose current one is the server's aria-current="page".

Example

Tabs
Source
<hui-tabs value="one">
<button slot="tab" role="tab" data-value="one">One</button>
<button slot="tab" role="tab" data-value="two">Two</button>
<div role="tabpanel" data-value="one">First panel</div>
<div role="tabpanel" data-value="two">Second panel</div>
</hui-tabs>

The Go template that renders it:

<hui-tabs value="{{ .Tab }}">
{{ range .Tabs }}
<button slot="tab" role="tab" data-value="{{ .ID }}">{{ .Label }}</button>
{{ end }}
{{ range .Tabs }}
<div role="tabpanel" data-value="{{ .ID }}">{{ .Body }}</div>
{{ end }}
</hui-tabs>

On a phone

Tabs that do not fit scroll their own row, with its scrollbar hidden, and the selected tab is scrolled into view on load and whenever it changes. The page does not scroll sideways.

Five tabs 375 px wide
Source
<hui-tabs value="notifications">
<button slot="tab" role="tab" data-value="overview">Overview</button>
<button slot="tab" role="tab" data-value="analytics">Analytics</button>
<button slot="tab" role="tab" data-value="reports">Reports</button>
<button slot="tab" role="tab" data-value="notifications">Notifications</button>
<button slot="tab" role="tab" data-value="settings">Settings</button>
<div role="tabpanel" data-value="overview">Overview panel</div>
<div role="tabpanel" data-value="analytics">Analytics panel</div>
<div role="tabpanel" data-value="reports">Reports panel</div>
<div role="tabpanel" data-value="notifications">Notifications panel</div>
<div role="tabpanel" data-value="settings">Settings panel</div>
</hui-tabs>

API

Attributes5
Attributes
NameTypeDefaultDescription
valuestring""The data-value (or value) of the selected tab. Reflected, and defaults to the first tab when unset or unknown.
orientation"horizontal" | "vertical""horizontal"The tablist axis, which decides whether Left/Right or Up/Down move focus.
activation"automatic" | "manual""automatic"automatic selects as focus moves; manual moves focus only and commits on Enter or Space.
variant"default" | "line""default"The list's look. default is the segmented list on a muted track. line is a bare row with no track and no raised chip, the active tab in full foreground under a 2px underline.
navigationbooleanfalseMakes the row a role="navigation" landmark of real links rather than a tablist. The element owns no panels, reads aria-current="page" but never writes it, and leaves activation to the link.
Properties5
Properties
NameTypeDefaultDescription
valuestring''Reflects to the value attribute.
orientationTabsOrientation'horizontal'Reflects to the orientation attribute.
activationTabsActivation'automatic'Reflects to the activation attribute.
variantTabsVariant'default'Reflects to the variant attribute.
navigationbooleanfalseReflects to the navigation attribute.
Events4
Events
NameDescription
hui-openA new tab was selected. It bubbles and is composed, with detail { value } for the new value.
hui-closeThe previous tab was deselected. It bubbles and is composed, with detail { value } for the previous value.
changeA native change event that bubbles and is composed, so Alpine x-model binds.
click on [slot="tab"]Clicking a tab selects its value. In navigation mode there is nothing to select: the link follows itself and none of these fire.
Slots2
Slots
NameDescription
tabThe role="tab" controls that make up the tablist, or the links in navigation mode.
(default)The role="tabpanel" panels. Unused in navigation mode.
CSS custom properties5
CSS custom properties
NameDefaultDescription
--hui-mutedoklch(0.96 0.003 325.6)Tablist background.
--hui-muted-foregroundoklch(0.542 0.034 322.5)Inactive tab text.
--hui-backgroundoklch(1 0 0)Selected tab background.
--hui-foregroundoklch(0.145 0.008 326)Selected tab text.
--hui-radius-mdcalc(var(--hui-radius, 0.45rem) * 0.8)Tablist corner radius.
::part() hooks1
::part() hooks
NameDescription
tablistThe internal wrapper. It carries role="tablist", or role="navigation" in navigation mode.

States

Line

variant="line" drops the track and the raised chip: a bare row, the active tab in full foreground under a 2px underline. It suits tabs nested inside a card, where a second segmented track competes with the first.

Line
Source
<hui-tabs variant="line" value="https">
<button slot="tab" role="tab" data-value="https">HTTPS</button>
<button slot="tab" role="tab" data-value="ssh">SSH</button>
<button slot="tab" role="tab" data-value="cli">GitHub CLI</button>
<div role="tabpanel" data-value="https">Clone using the web URL.</div>
<div role="tabpanel" data-value="ssh">Use a password-protected SSH key.</div>
<div role="tabpanel" data-value="cli">Work fast with the official CLI.</div>
</hui-tabs>

Vertical, manual activation

orientation="vertical" moves with Up and Down. activation="manual" separates moving from choosing: the arrows move focus, and Enter or Space switches the panel.

Vertical, manual activation
Source
<hui-tabs orientation="vertical" activation="manual" value="a">
<button slot="tab" role="tab" data-value="a">Alpha</button>
<button slot="tab" role="tab" data-value="b">Beta</button>
<div role="tabpanel" data-value="a">Alpha panel</div>
<div role="tabpanel" data-value="b">Beta panel</div>
</hui-tabs>

Server navigation

With navigation the row is a role="navigation" landmark of real links, not a tablist. The server marks the current link with aria-current="page"; the element owns no panels and writes no aria-selected. The arrows move focus and Enter follows the link, so a host swaps a whole workflow rather than one panel. This is UI-CONTRACT-007: the markup is the state. A Go template emits it directly: {{ range .Steps }}<a slot="tab" href="{{ .URL }}"{{ if .Current }} aria-current="page"{{ end }}>{{ .Label }}</a>{{ end }}.

Server navigation
Source
<hui-tabs navigation variant="line" aria-label="Workflow">
<a slot="tab" href="/overview" aria-current="page">Overview</a>
<a slot="tab" href="/runs">Runs</a>
<a slot="tab" href="/settings">Settings</a>
</hui-tabs>

Server-side mechanics

The server owns the tabs and panels: role="tab" children in slot="tab" and role="tabpanel" children in the default slot, each carrying data-value (or value), plus value, orientation and activation. Render <hui-tabs value="two"> to open a given tab, and read value back because it reflects; an unknown or missing value falls back to the first tab. It is not form-associated and submits nothing, though selecting a tab emits bubbling, composed hui-open and hui-close events plus a native change for Alpine x-model. Panels are light-DOM children, so an hx-swap can replace a panel's contents in place, with the ids and aria-controls/aria-labelledby re-synced on the slot's change event. A swap that replaces the whole hui-tabs element needs the server to emit value again, because the component keeps no state of its own.

Accessibility

  • The internal wrapper carries role="tablist" and aria-orientation.
  • Each tab gets aria-selected and aria-controls; each panel gets aria-labelledby; ids are generated when absent.
  • Inactive panels are hidden and marked data-state="inactive".
  • Roving tabindex keeps the tablist a single tab stop, with exactly one tab carrying tabindex="0".
  • In navigation mode the wrapper is a role="navigation" landmark with the host's aria-label; a row of links is not a tablist, so no tab or tabpanel roles are invented and the current link is the server's aria-current="page".

Keyboard

Keyboard
KeysAction
Arrow Left, Arrow RightHorizontal orientation: move focus between tabs, wrapping. With automatic activation the panel follows focus.
Arrow Up, Arrow DownVertical orientation: move focus between tabs, wrapping.
Home, EndMove focus to the first or last tab.
Enter, SpaceIn manual activation, commit the focused tab without waiting for the next arrow press.
Enter (navigation mode)Follows the focused link, natively, as the browser does for any anchor. The arrows move focus and the tab stop without changing the server's aria-current.

Gotchas

  • Selection is reported with the event names hui-open and hui-close, not hui-select or change alone, so a listener expecting a value-carrying event will miss it.