Tabs

A headless, accessible tabs primitive with automatic activation, roving tabindex, and composable Root, List, Trigger, Panel, and Content parts.

Features

  • Headless
  • Automatic activation
  • Horizontal or vertical
  • Disabled triggers
  • Accessible tab panels
  • Composable parts

Installation

npm install @ariaui/tabs

Examples

Automatic activation with roving tabindex: compose List, Trigger, Panel, and Content in order (each trigger maps to the Content at the same index).

Playgrounds mirror the legacy doc set—authentication forms, compact account panels, home/settings, preview/code with lucide-react icons, and billing plus a badge-styled notifications tab.

Default

Sign-in and sign-up forms with a pill tab list, checkbox, and secondary actions.

Preview

No preview output yet.

live.tsxReady

Framer Motion

A Framer Motion tabs example with a sliding active tab background.

Preview

No preview output yet.

live.tsxReady

Anatomy

tsx
import * as Tabs from "@ariaui/tabs";

export default function Example() {
  return (
    <Tabs.Root>
      <Tabs.List>
        <Tabs.Trigger />
      </Tabs.List>
      <Tabs.Panel>
        <Tabs.Content />
      </Tabs.Panel>
    </Tabs.Root>
  );
}

API Reference

Root

Context provider and state owner for the tabs. Manages the active value and coordinates trigger registration. Uses automatic activation — arrow keys both move focus and change the active panel.

PropTypeDefault
asChild
booleanfalse
defaultValue
string-
value
string-
onValueChange
(value: string) => void-
orientation
'horizontal' | 'vertical''horizontal'

List

Tablist container. Wraps the Trigger elements.

PropTypeDefault
asChild
booleanfalse
AttributeValues
role'tablist'
aria-orientation'horizontal' | 'vertical' — mirrors Root's orientation

Trigger

A tab button that activates its associated panel. Only the active Trigger is in the tab order; inactive Triggers are skipped by roving focus.

PropTypeDefault
value*
string-
disabled
booleanfalse
asChild
booleanfalse
AttributeValues
role'tab'
aria-selected'true' when active, 'false' otherwise
aria-controlsID of the associated Panel tabpanel element
aria-disabledpresent when `disabled`

Panel

Renders one `role="tabpanel"` container per `Content` child. Inactive panels receive the `hidden` attribute; props are forwarded only to the active panel.

AttributeValues
role'tabpanel' (on each generated panel container)
aria-labelledbyID of the associated Trigger
hiddenpresent on inactive panels

Content

Structural content slot for a single tab panel. Does not apply tabpanel semantics — those live on the surrounding Panel.

PropTypeDefault
value*
string-
asChild
booleanfalse

Keyboard

ShortcutAction
Move focus and selection to the next tab. Wraps from last to first.
Move focus and selection to the previous tab. Wraps from first to last.
HomeMove focus and selection to the first tab.
EndMove focus and selection to the last tab.
TabMove focus from the active Trigger into the active Panel.

Accessibility

Tabs follows the WAI-ARIA Tabs pattern with automatic activation:

  • List renders with role="tablist" and mirrors Root's orientation onto aria-orientation.
  • Each Trigger renders with role="tab", toggles aria-selected based on the active index, and is wired to its panel via aria-controls.
  • Panel renders one role="tabpanel" container per Content child, with aria-labelledby pointing back at the associated Trigger. Inactive panels get the native hidden attribute.
  • Roving tabindex — only the active Trigger is in the tab order (tabindex="0"); the rest are -1. Tab from a Trigger moves focus into the active Panel's content, not to the next Trigger.
  • Arrow keys move focus and change the selected tab (automatic activation). ArrowRight / ArrowLeft wrap at the ends; Home and End jump to the first and last Trigger.
  • Disabled Triggers receive aria-disabled and are skipped by arrow-key navigation, so focus never lands on a dead tab.
  • Make sure every Trigger has a visible, human-readable label — avoid icon-only triggers without aria-label, since screen readers announce the Trigger's accessible name as the tab title.
Previous
Table