Skip to main content

Tabs

A composable, accessible tabs component implementing compound component architecture, controlled/uncontrolled state, roving tabindex keyboard navigation, ARIA-linked triggers and panels, and responsive overflow handling.


Usage

Basic usage

import { Tabs } from '@nofinite/nui';

<Tabs defaultValue="account">
<Tabs.List>
<Tabs.Trigger value="account">Account</Tabs.Trigger>
<Tabs.Trigger value="password">Password</Tabs.Trigger>
</Tabs.List>

<Tabs.Content value="account">Account content</Tabs.Content>
<Tabs.Content value="password">Password content</Tabs.Content>
</Tabs>;

Controlled usage

const [tab, setTab] = useState('account');

<Tabs value={tab} onChange={setTab}>
<Tabs.List>
<Tabs.Trigger value="account">Account</Tabs.Trigger>
<Tabs.Trigger value="billing">Billing</Tabs.Trigger>
</Tabs.List>

<Tabs.Content value="account">Account</Tabs.Content>
<Tabs.Content value="billing">Billing</Tabs.Content>
</Tabs>;

Disabled tab

<Tabs defaultValue="a">
<Tabs.List>
<Tabs.Trigger value="a">A</Tabs.Trigger>
<Tabs.Trigger value="b" disabled>
B
</Tabs.Trigger>
</Tabs.List>

<Tabs.Content value="a">Content A</Tabs.Content>
</Tabs>

Scrollable mobile tabs

<Tabs defaultValue="tab1">
<Tabs.List>
{items.map((i) => (
<Tabs.Trigger key={i} value={i}>
{i}
</Tabs.Trigger>
))}
</Tabs.List>
</Tabs>

Props

Tabs Root

PropTypeDefaultDescription
valuestringControlled selected tab
defaultValuestring""Initial selected tab
onChange(value:string)=>voidValue change handler
childrenReactNodeCompound children
classNamestringStyling override

Tabs List

PropTypeDefaultDescription
childrenReactNodeTab triggers
classNamestringStyling override

Tabs Trigger

PropTypeDefaultDescription
valuestringAssociated tab value
disabledbooleanfalseDisables trigger
classNamestringStyling override

Tabs Content

PropTypeDefaultDescription
valuestringPanel value
classNamestringStyling override

Variants

Control variants

<Tabs defaultValue="a" />
<Tabs value="a" onChange={()=>{}} />

Trigger variants

<Tabs.Trigger value="a" />
<Tabs.Trigger value="b" disabled />

Layout variants

<Tabs.List />
<Tabs.List className="scrollable" />

Content rendering variants

<Tabs.Content value="a" />
<Tabs.Content value="b" />

Available variants

  • Controlled tabs
  • Uncontrolled tabs
  • Disabled trigger tabs
  • Scrollable mobile tabs
  • Auto-select on focus tabs
  • Animated content tabs
  • Compound composition tabs

Guidelines

  • Prefer controlled mode for router-driven tabs
  • Use disabled triggers for unavailable sections
  • Keep tab count reasonable on mobile
  • Provide semantic labels for accessibility
  • Avoid heavy mounting cost inside tab content

States

  • Default
  • Hover trigger
  • Selected trigger
  • Disabled trigger
  • Focus-visible trigger
  • Content mounted
  • Content transitioning

Keyboard Interaction

  • ArrowRight → next tab
  • ArrowLeft → previous tab
  • Home → first tab
  • End → last tab
  • Tab → moves into panel
  • Focus automatically activates tab (roving tabindex)

Accessibility

  • Compound ARIA tab pattern
  • role="tablist", role="tab", role="tabpanel"
  • aria-selected reflects state
  • aria-controls links trigger → panel
  • aria-labelledby links panel → trigger
  • Stable ID prefix ensures SR compatibility
  • Only active tab is tabbable
  • Disabled tabs excluded from roving navigation

Design Tokens

TokenUsage
--nui-border-defaultTab list divider
--nui-border-hoverHover indicator
--nui-color-primaryActive indicator
--nui-fg-defaultText
--nui-fg-subtleInactive text
--nui-brand-200Focus ring
--nui-space-*Spacing scale
--nui-radius-smFocus rounding
--nui-font-sansTypography

Best Practices

Do

  • Use controlled tabs for URL-sync navigation
  • Keep tab labels concise
  • Mount heavy content lazily if expensive
  • Provide disabled state when needed
  • Use unique tab values

Don’t

  • Duplicate tab values
  • Use index as value
  • Nest interactive elements inside triggers
  • Hide essential actions inside non-selected panels
  • Mount large data grids inside all panels simultaneously