Skip to content

Tabs

NavigationSince 0.2.0
import { Tabs } from '@absoluteui/components/tabs';
import '@absoluteui/components/tabs/style.css';

Using Angular? See Tabs for Angular.

Switch between peer views of related content. Tabs implements the WAI-ARIA APG Tabs pattern: a labelled tablist of native buttons with a roving tabindex, and one tabpanel per tab. Activation is automatic (select on focus) or manual (Enter/Space), orientation is horizontal or vertical, arrow keys are mirrored in RTL, and a horizontal tablist that overflows scrolls with the selected tab kept in view.

  • Switching between peer views of related content in the same context, such as Overview, Activity, and Settings.
  • Reducing page length when users need only one section at a time.
  • Sequential steps: use a stepper or wizard.
  • Navigating between pages or routes: use links (navigation bar, breadcrumb).
  • Content users need to compare side by side.
  • Many sections users read in order: use headings or an accordion.
import { Tabs } from '@absoluteui/components/tabs';
const host = document.createElement('div');
document.body.append(host);
Tabs.create(host, {
label: 'Account',
tabs: [
{ id: 'overview', label: 'Overview', content: 'Plan, usage, and contacts.' },
{ id: 'billing', label: 'Billing', icon: 'info', content: 'Invoices and payment methods.' },
{ id: 'audit', label: 'Audit log', disabled: true },
],
});

Manual activation with lazily filled panels

Section titled “Manual activation with lazily filled panels”
import { Tabs, type TabsChangeDetail } from '@absoluteui/components/tabs';
const host = document.createElement('div');
document.body.append(host);
const tabs = Tabs.create(host, {
label: 'Reports',
activation: 'manual',
tabs: [
{ id: 'daily', label: 'Daily' },
{ id: 'weekly', label: 'Weekly' },
],
});
const fill = (id: string): void => {
const panel = tabs.panel(id);
if (panel && panel.childElementCount === 0) {
const table = document.createElement('table');
panel.append(table);
}
};
fill(tabs.value);
host.addEventListener('aui-change', (event) => {
fill((event as CustomEvent<TabsChangeDetail>).detail.value);
});
import { Tabs } from '@absoluteui/components/tabs';
const host = document.createElement('div');
document.body.append(host);
const settings = Tabs.create(host, {
label: 'Settings',
orientation: 'vertical',
tabs: [
{ id: 'profile', label: 'Profile' },
{ id: 'security', label: 'Security' },
{ id: 'notifications', label: 'Notifications' },
],
value: 'security',
});
settings.select('notifications');
import { Tabs, type TabsChangeDetail } from '@absoluteui/components/tabs';
import '@absoluteui/components/tabs/style.css';
const host = document.querySelector<HTMLElement>('#report-tabs');
if (host) {
const tabs = Tabs.create(host, { label: 'Reports', tabs: [{ id: 'daily', label: 'Daily' }, { id: 'weekly', label: 'Weekly' }] });
host.addEventListener('aui-change', (event) => {
const panel = tabs.panel((event as CustomEvent<TabsChangeDetail>).detail.value);
if (panel && panel.childElementCount === 0) panel.append(document.createElement('table'));
});
}
import { Tabs } from '@absoluteui/components/tabs';
const host = document.createElement('div');
document.body.append(host);
Tabs.create(host, {
label: 'Account',
tabs: [
{ id: 'overview', label: 'Overview', content: 'Plan, usage, and contacts.' },
{ id: 'billing', label: 'Billing', content: 'Invoices and payment methods.' },
],
});
<div class="aui-tabs aui-tabs--horizontal"> (consumer host)
<div class="aui-tabs__list" role="tablist" aria-label aria-orientation>
<button class="aui-tabs__tab" role="tab" id aria-selected aria-controls tabindex>
<svg class="aui-icon" aria-hidden="true"> (optional)
<span class="aui-tabs__label">Label</span>
</button>
...
</div>
<div class="aui-tabs__panels">
<div class="aui-tabs__panel" role="tabpanel" id aria-labelledby tabindex="0"> (selected)
<div class="aui-tabs__panel" role="tabpanel" hidden> (inactive)
</div>
</div>
Tabs.create(host: HTMLElement, options: TabsOptions): Tabs

Global configuration key: tabs (see Configuration).

Option Type Default Description
label string Accessible name of the tablist (aria-label). Required.
tabs readonly { id: string; label: string; icon?: IconRef; disabled?: boolean; content?: Node | string }[] Tabs in order. Ids must be unique; they are consumer keys, not DOM ids. Required.
value string Selected tab id. Default: the first enabled tab.
activation 'automatic' | 'manual' "automatic" automatic selects on focus; manual needs Enter or Space. Globally configurable.
orientation 'horizontal' | 'vertical' "horizontal" Layout and arrow keys (Left/Right or Up/Down). Globally configurable.
direction 'ltr' | 'rtl' "ltr" Mirrors Left/Right in rtl. An ancestor dir=rtl also mirrors. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement Consumer host; its children are owned by the component.
tablist HTMLDivElement The role=tablist element.
value string Selected tab id.
options Readonly<TabsOptions> Current instance options.
Method Description
panel(id: string): HTMLDivElement | null The tabpanel for a tab id, for consumers to fill.
select(id: string): void Select programmatically (no events).
update(options: Partial<TabsOptions>): void Merge and apply options. Panels are kept by id.
destroy(): void Remove rendering, listeners, observers, and subscriptions.
Event Detail Bubbles Cancelable Description
aui-change { value: string; previous: string } Yes No The user selected a different tab.
Area Description
panel One tabpanel per tab: tabs.panel(id), or the tab’s content option.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-tabs Host.
aui-tabs--horizontal | --vertical Orientation modifiers on the host.
aui-tabs__list role=tablist.
aui-tabs__tab role=tab button.
aui-tabs__label Tab label text.
aui-tabs__panels Panel container.
aui-tabs__panel role=tabpanel.

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-border-width-strong | --aui-color-border | --aui-color-focus-ring | --aui-color-hover | --aui-color-primary | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-weight-semibold | --aui-icon-size-sm | --aui-line-height-normal | --aui-space-1 | --aui-space-2 | --aui-space-4 | --aui-touch-target-min

type TabsActivation = 'automatic' | 'manual';
type TabsOrientation = 'horizontal' | 'vertical';
interface TabItem { id: string; label: string; icon?: IconRef; disabled?: boolean; content?: Node | string }
interface TabsChangeDetail { value: string; previous: string }

Pattern: WAI-ARIA APG Tabs pattern (automatic and manual activation), with native buttons as tabs.

Key Context Action
Tab Focus outside the tablist Moves focus to the selected tab (single tab stop).
Tab Tab focused Moves focus into the active panel (or its first focusable element).
ArrowRight / ArrowLeft Horizontal tablist Next / previous enabled tab, wrapping; mirrored in RTL. Automatic mode also selects it.
ArrowDown / ArrowUp Vertical tablist Next / previous enabled tab, wrapping. Automatic mode also selects it.
Home / End Tab focused First / last enabled tab. Automatic mode also selects it.
Enter / Space Tab focused (manual activation) Selects the focused tab.
Element Role
tab container tablist
<button> tab
panel tabpanel
Attribute Element Value
aria-label tablist label option
aria-orientation tablist horizontal | vertical
aria-selected tab true on the selected tab, false otherwise
aria-controls tab id of its tabpanel
aria-labelledby tabpanel id of its tab
tabindex tabpanel 0 when the panel contains no focusable element
disabled tab Native disabled for disabled tabs (skipped by arrow keys)

One tab stop on the selected tab. In manual mode focus can move without selecting; when focus leaves the tablist, the tab stop returns to the selected tab. Re-rendering never moves or blurs the focused tab.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.1 Use of Color A
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • Selection is conveyed by aria-selected, an indicator bar, and heavier weight (not color alone).
  • Inactive panels use the hidden attribute, so they are removed from the accessibility tree.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.

default, hover, focus-visible, selected, disabled, overflowing (scrollable tablist).

From Trigger To
tab A selected click tab B, or arrow to B (automatic) B selected; aui-change
tab A selected arrow to B (manual) A still selected; B focused with the tab stop
B focused (manual) Enter / Space B selected; aui-change
B focused (manual) focus leaves the tablist tab stop returns to the selected tab
any select(id) / update({ value }) selection changes; no event
selected tab removed by update({ tabs }) re-render first enabled tab selected; no event
  • Selection is shown by an indicator bar and semibold weight, not by color alone (WCAG 1.4.1).
  • Re-rendering never re-inserts a focused tab, so focus is not lost during automatic activation.
  • Icons are decorative; the label text is the accessible name.
  • Manual screen-reader verification is pending (see Known limitations).

The horizontal tablist never wraps. It scrolls horizontally (overflow-x: auto, thin scrollbar), and after every selection the tablist, never the page, is scrolled so the selected tab is fully visible. Tabs keep the minimum touch-target height.

Logical properties throughout. ArrowLeft/ArrowRight are mirrored when direction is 'rtl' or when the host inherits dir="rtl". The vertical indicator is on the inline-start edge.

Colors come from role tokens only. Verified in the light, dark, and high-contrast themes. In forced-colors mode the selected indicator uses Highlight and disabled tabs use GrayText.

configureAbsoluteUI({ components: { tabs: { activation: 'manual' } } });

Changes apply to existing instances immediately.

Instance options override global and theme defaults. All options are dynamic.

Each tab accepts an optional decorative icon (IconRef). There are no built-in icon slots.

Labels and string content use textContent; HTML strings are never parsed. Node content is inserted as provided, so the consumer owns its safety. CSP-safe: no inline styles or handlers.

No persistence, network access, or telemetry.

Three listeners on the tablist (click, keydown, focusout), one MutationObserver on the panel container, and one configuration subscription per instance. Re-renders reuse tab and panel elements by id. Budget: under 2 ms for Tabs.create with 20 tabs.

Tested with 20 tabs in an overflowing tablist. Beyond about 10 tabs, consider a different navigation pattern.

  • If every tab is disabled, the first tab is selected and the tablist has no tab stop.
  • A selected tab that becomes disabled stays selected; the tab stop moves to the first enabled tab.
  • Removing the selected tab through update({ tabs }) selects the first enabled tab without an event.
  • Consumer ids can be any non-empty string; DOM ids are generated separately.
Code Category Cause
TABS_OPTIONS configuration Missing label, empty tabs, duplicate or empty ids, empty tab labels, or a value/select(id) that is not a tab id.
CFG_INVALID_VALUE configuration Invalid activation, orientation, or direction.

A failed construction leaves no rendering, listeners, or subscriptions behind. A failed update() keeps the previous state.

  • Panels are always in the DOM; lazy loading is the consumer’s job (for example on aui-change).
  • No closable, reorderable, or user-added tabs.
  • No scroll buttons for an overflowing tablist.
  • Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • HTML strings as labels or content.
  • Tabs that navigate to separate URLs.