Tabs
A set of panels, one shown at a time, switched between via a strip of tabs — with arrow keys moving focus and selection together, as the WAI-ARIA Tabs pattern specifies.
Update your display name and the email you sign in with.
Usage#
import { Tabs, TabsContent, TabsList, TabsTrigger } from "neelam-ui";
<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">Update your display name.</TabsContent>
<TabsContent value="password">Change your password.</TabsContent>
</Tabs>Each TabsContent is matched to its TabsTrigger by value.
Orientation#
orientation="vertical" stacks the strip beside the panels. The arrow keys
follow: Up/Down replace Left/Right, and aria-orientation is set to match, so
assistive tech announces the right ones.
Workspace name, language, and timezone.
Automatic activation#
Arrow keys move focus and selection together — "automatic activation" in the pattern's terms. Arrowing along the strip switches panels as you go, the way a browser's own tab strip behaves.
That differs from the roving focus in
Select or
DropdownMenu, where arrows only move focus
and a separate Enter confirms. The distinction is deliberate: a menu item
might be destructive or hard to undo, so it should not fire just because you
moved through it. Switching a tab is cheap and reversible.
Automatic activation assumes cheap panels
If a panel triggers an expensive fetch on mount, arrowing across five tabs fires five requests. Cache or debounce the load rather than changing the interaction — the keyboard behaviour here is what the pattern expects.
Inactive panels are unmounted#
Only the active TabsContent is rendered at all. Unlike
Accordion, which keeps closed content mounted so
it can animate, switching tabs is not animated, so there is nothing to gain from
keeping inactive panels around.
The trade-off is real and worth stating plainly: a panel's internal state — a scroll position, an uncontrolled input's value — is lost when you switch away and will not be there when you switch back. Lift anything that must survive into your own state.
Controlled#
const [tab, setTab] = useState("account");
<Tabs value={tab} onValueChange={setTab}>…</Tabs>Keyboard#
| Key | Behaviour |
|---|---|
| Tab | Moves focus to the active tab, then past the whole strip to the panel — not through every tab. |
| ←→ | Moves to the previous or next tab, wrapping at the ends. Horizontal orientation. |
| ↑↓ | The same, when orientation="vertical". |
| HomeEnd | Jumps to the first or last tab. |
| EnterSpace | Activates the focused tab — though arrowing has already selected it. |
Accessibility#
Implements the APG tabs pattern:
- The strip is
role="tablist"witharia-orientation; each tab is a native<button>recategorized asrole="tab"witharia-selectedandaria-controlspointing at its panel. - Roving tabindex: only the active tab is in the page's tab order
(
tabIndex={0}), the rest are-1.Tabtherefore steps past the strip rather than through every tab in it. - Each panel is
role="tabpanel"withtabIndex={0}, so a keyboard user can move from the tab straight into the panel even when its content has nothing focusable in it. - Disabled tabs are skipped by the arrow keys rather than trapping focus.
API reference#
Tabs#
| Prop | Type | Default |
|---|---|---|
defaultValue | string | — |
onValueChange | ((value: string) => void) | — |
orientationDefaults to `"horizontal"`. | Orientation | "horizontal" |
valueControls which tab is active. Omit to let the tabs manage their own state. | string | — |
TabsList#
| Prop | Type | Default |
|---|---|---|
defaultValue | string | — |
onValueChange | ((value: string) => void) | — |
orientationDefaults to `"horizontal"`. | enum | horizontal |
valueControls which tab is active. Omit to let the tabs manage their own state. | string | — |
TabsTrigger#
| Prop | Type | Default |
|---|---|---|
valuerequired | string | — |
TabsContent#
| Prop | Type | Default |
|---|---|---|
valuerequired | string | — |