apx-dsv0.1Local renderer · live
K
Foundations4
  • Getting started
  • Theming
  • Templates
  • Icons
  • Avatar
  • Badge
  • DataGrid
  • Scheduler
  • Stat
  • Table
  • Timeline
  • TreeView
  • Accordion
  • Alert
  • EmptyState
  • Progress
  • Skeleton
  • Spinner
  • SplashScreen
  • Toast
  • ColorPicker
  • FileUpload
  • Form
  • Rating
  • TagsInput
  • Combobox
  • Field
  • Select
  • Toggle
  • Button
  • Calendar
  • Checkbox
  • DatePicker
  • Input
  • NumberInput
  • Radio
  • Slider
  • Switch
  • Textarea
  • AppShell
  • Div
  • Divider
  • Sidebar
  • Stack
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
60 componentsapx-ds/renderer
Tabs
Variant↳ other

Navigation

Tabs

Sectioned navigation primitive. Compound —

Overview

<Tabs> is the canonical sectioned-navigation primitive. It implements the W3C ARIA Tabs pattern end-to-end — roving tabindex, arrow-key navigation, manual vs automatic activation, panel association — across four visual variants and two orientations.

The compound shape mirrors <Card>: a root state owner with three dot-namespaced subparts that read everything they need from a single context.

Overview — three tabs with realistic panel content

Loading preview…
Overview.tsx
tsx
import { Tabs } from 'apx-ds';

<Tabs defaultValue="overview">
  <Tabs.List aria-label="Sections">
    <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
    <Tabs.Trigger value="activity">Activity</Tabs.Trigger>
    <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Panel value="overview">…</Tabs.Panel>
  <Tabs.Panel value="activity">…</Tabs.Panel>
  <Tabs.Panel value="settings">…</Tabs.Panel>
</Tabs>;
import { Tabs } from 'apx-ds';

<Tabs defaultValue="overview">
  <Tabs.List aria-label="Sections">
    <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
    <Tabs.Trigger value="activity">Activity</Tabs.Trigger>
    <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Panel value="overview">…</Tabs.Panel>
  <Tabs.Panel value="activity">…</Tabs.Panel>
  <Tabs.Panel value="settings">…</Tabs.Panel>
</Tabs>;

Anatomy

SubpartElementRole
<Tabs><div>container, state owner
<Tabs.List><div role="tablist">trigger row — put aria-label here so SR users hear the navigation name
<Tabs.Trigger><button role="tab">activator (becomes the wrapped child when asChild)
<Tabs.Panel><div role="tabpanel">content region paired with a trigger of matching value

Examples

The default example below is fully interactive — click any trigger or use the keyboard (Tab to enter, Arrow keys to navigate, Home / End to jump).

Basic

Loading preview…
Basic.tsx

Variants

Loading preview…
Variants.tsx

Sizes

Loading preview…
Sizes.tsx

Colors

Loading preview…
Colors.tsx

Vertical

Loading preview…
Vertical.tsx

WithIcons

Loading preview…
WithIcons.tsx

WithBadges

Loading preview…
WithBadges.tsx

Disabled

Loading preview…
Disabled.tsx

ManualActivation

Loading preview…
ManualActivation.tsx

FullWidth

Loading preview…
FullWidth.tsx

AsChildRouting

Loading preview…
AsChildRouting.tsx

ForceMount

Loading preview…
ForceMount.tsx

Controlled

Loading preview…
Controlled.tsx

Props

PropTypeDefaultDescription
activationenum'automatic'Arrow-key activation behavior.
alignmentenum'start'Justification of the trigger row inside `<Tabs.List>`.
colorenum'primary'Semantic palette role for the active accent.
defaultValuestring—Uncontrolled initial active panel value.
fullWidthbooleanfalseStretch the trigger row to fill its container.
onValueChange((value: string) => void)—Fires with the new value whenever the active tab changes.
orientationenum'horizontal'Layout axis.
sizeResponsiveValue<TabsSize>'md'Trigger padding + font size.
sxSx—Theme-aware inline style object.
valuestring—Controlled active panel value. Mirrors the `value` of one of the child `<Tabs.Trigger>`s.
variantResponsiveValue<TabsVariant>'underline'Stylistic family of the triggers.

Activation modes

tsx
<Tabs activation="automatic">…</Tabs>  // default — arrow keys change focus AND active panel
<Tabs activation="manual">…</Tabs>     // arrow keys only move focus; Enter/Space activates
<Tabs activation="automatic">…</Tabs>  // default — arrow keys change focus AND active panel
<Tabs activation="manual">…</Tabs>     // arrow keys only move focus; Enter/Space activates

Pick manual when activating a panel is expensive (fetches, large lists) and you don't want every arrow-key press to trigger that work. The ManualActivation example above demonstrates the difference live.

Keyboard

  • Tab — enters the tablist on the active trigger; leaves to the next focusable on the page.
  • Arrow Left / Right — horizontal navigation (RTL flips the direction).
  • Arrow Up / Down — vertical navigation.
  • Home / End — jump to first / last enabled trigger.
  • Enter / Space — explicit activation in activation="manual" mode.
  • Disabled triggers are skipped by keyboard navigation.

Accessibility

  • <Tabs.List> carries role="tablist" + aria-orientation. Put the aria-label on the list itself — that's the role screen readers announce.
  • Each <Tabs.Trigger> is role="tab" with aria-selected, aria-controls, aria-disabled, and a stable id.
  • Each <Tabs.Panel> is role="tabpanel" with aria-labelledby pointing at its trigger and tabIndex={0} so SR users can land inside.
  • Roving tabindex: only the active trigger has tabIndex={0}; siblings have -1.
  • All IDs are derived from a single useId() call so SSR + hydration match.
  • axe-core: zero violations across the variant × orientation matrix.

Routing-driven tabs

tsx
import Link from 'next/link';

<Tabs defaultValue="/overview">
  <Tabs.List aria-label="Sections">
    <Tabs.Trigger asChild value="/overview">
      <Link href="/overview">Overview</Link>
    </Tabs.Trigger>
    <Tabs.Trigger asChild value="/activity">
      <Link href="/activity">Activity</Link>
    </Tabs.Trigger>
  </Tabs.List>
  <Tabs.Panel value="/overview">…</Tabs.Panel>
  <Tabs.Panel value="/activity">…</Tabs.Panel>
</Tabs>;
import Link from 'next/link';

<Tabs defaultValue="/overview">
  <Tabs.List aria-label="Sections">
    <Tabs.Trigger asChild value="/overview">
      <Link href="/overview">Overview</Link>
    </Tabs.Trigger>
    <Tabs.Trigger asChild value="/activity">
      <Link href="/activity">Activity</Link>
    </Tabs.Trigger>
  </Tabs.List>
  <Tabs.Panel value="/overview">…</Tabs.Panel>
  <Tabs.Panel value="/activity">…</Tabs.Panel>
</Tabs>;

asChild swaps the rendered element while preserving every ARIA role, the active-state classes, the roving tabindex, and the click handler. The wrapped element supplies the href and routing behavior. See the AsChildRouting example above.

forceMount

tsx
<Tabs.Panel value="player" forceMount>
  <Video />
</Tabs.Panel>
<Tabs.Panel value="player" forceMount>
  <Video />
</Tabs.Panel>

Inactive forceMount panels are hidden via the hidden attribute, so they're removed from the a11y tree but their React subtree (and any DOM state) stays intact. Useful for video players or expensive children you don't want to remount per tab switch. The ForceMount example above demonstrates the pattern.

Theming

Each of the four slots can be overridden independently:

tsx
<ThemeProvider
  theme={defineTheme({
    components: {
      Tabs: {
        defaultProps: { variant: 'pills', size: 'sm' },
        styleOverrides: {
          root: 'gap-1',
          list: 'bg-bg-subtle p-1 rounded-md border-0',
          trigger: 'font-semibold',
          panel: 'mt-6',
        },
      },
    },
  })}
>
  {children}
</ThemeProvider>
<ThemeProvider
  theme={defineTheme({
    components: {
      Tabs: {
        defaultProps: { variant: 'pills', size: 'sm' },
        styleOverrides: {
          root: 'gap-1',
          list: 'bg-bg-subtle p-1 rounded-md border-0',
          trigger: 'font-semibold',
          panel: 'mt-6',
        },
      },
    },
  })}
>
  {children}
</ThemeProvider>

Instance-level overrides win via tailwind-merge:

tsx
<Tabs className="border rounded-md" sx={{ radius: 'lg' }}>
  …
</Tabs>
<Tabs className="border rounded-md" sx={{ radius: 'lg' }}>
  …
</Tabs>

RTL

The keyboard handler reads dir="rtl" from the nearest ancestor at handle time and flips horizontal arrow-key direction accordingly. Vertical orientation uses the logical border-e divider, which automatically flips visually.

Do / Don't

  • Do treat value as an opaque identifier — strings are fine; URL paths are fine.
  • Do use activation="manual" when switching panels triggers network work.
  • Do put aria-label on <Tabs.List> (or use aria-labelledby) — that's the role SR users hear.
  • Don't put critical content behind a tab the user might miss; use a heading
    • section if the content always matters.
  • Don't auto-select on mount in shared rendering contexts (e.g. SSR-only); rely on defaultValue from your data.