|Kit

Tabs

A navigation control on Ark UI's Tabs machine — a row (or column) of triggers, each revealing its own content panel, with a pill that slides to the active trigger. Visually SegmentGroup's twin.

Tabs is a navigation control: a row (or column) of triggers, each revealing its own content panel, with a pill that slides to the active trigger. Visually it's SegmentGroup's twin — the same filled / light / outline / subtle / default variant × color system and the same sliding-pill mechanics — but built for real content panels instead of a single-choice value, with no Field integration (Tabs is navigation, not a form control).

import { Tabs } from '@42-central/ui-react/tabs';

<Tabs
  defaultValue="overview"
  data={[
    { value: 'overview', label: 'Overview', content: <Overview /> },
    { value: 'activity', label: 'Activity', content: <Activity /> },
  ]}
/>

Playground

Component

Loading preview

Props

Code

<Component
  orientation="horizontal"
  variant="default"
  size="md"
/>

Items

data is a flat array of { value, label, content, disabled? } — each tab needs its own content, so (unlike SegmentGroup) there's no bare-string shorthand.

<Tabs
  defaultValue="overview"
  data={[
    { value: 'overview', label: 'Overview', content: <Overview /> },
    { value: 'activity', label: 'Activity', content: <Activity /> },
    { value: 'settings', label: 'Settings', content: <Settings />, disabled: true },
  ]}
/>

Each item also accepts an optional testId, defaulting to `tab-${value}` — the matching content panel gets `${testId}-panel`. See the Testing guide. The compound Tabs.Trigger / Tabs.Content / Tabs.List / Tabs.Root / Tabs.Indicator parts each take their own testId prop directly, for the same reason.

Variants

Five treatments, mirroring Button and SegmentGroup. The variant restyles the tray, every trigger, and the sliding pill off the same --c-* color slots. filled is a solid pill, light a soft tint, outline a ringed pill over the tray, subtle is chromeless. default is the same solid pill as filled on a lighter tray — with no color set it resolves to black (white in dark theme), and it tints like every other variant when you give it one.

A
A
A
A
A

Code

<Tabs variant="filled" color="brand" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="light" color="brand" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="outline" color="brand" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="subtle" color="brand" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="default" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />

Sizes

Five sizes — xs through xl — matching the rest of the family. Sizing lives on the triggers; the tray's own gap scales alongside.

A
A
A
A
A

Code

<Tabs size="xs" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs size="sm" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs size="md" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs size="lg" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs size="xl" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />

Colors

The resting triggers are palette-independent; only the sliding pill (and the active label's text) adopt color. Set it once and every kit palette tints for free — the active label follows the pill onto --c-on-solid, so it stays legible whichever palette you pick. color is scoped to the tab strip: it does not tint whatever your panels render, so a Button inside a panel keeps its own palette.

A
A
A
A
A

Code

<Tabs variant="filled" color="brand" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="filled" color="green" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="filled" color="blue" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="filled" color="purple" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="default" color="teal" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />

Orientation

Set orientation="vertical" to stack the triggers in a column instead of a row.

A quick summary of the project — status, owners, and the last update.

Disabled tab

A single tab can be disabled via { value, label, content, disabled: true }. It's skipped by keyboard navigation and can't be clicked.

Available to everyone.

Compound API

Drop to the composable parts for anything the flat data array can't express — custom trigger content, interleaved non-tab elements, or driving the value from your own state. variant, size and color all live on Tabs.List and apply to the tray, every trigger, and the indicator. color is a data-color attribute on the list specifically — putting it on Tabs.Root would also cover every Tabs.Content, leaking the palette into your panel content.

'use client';
import { Tabs } from '@42-central/ui-react/tabs';

<Tabs.Root defaultValue="overview">
  <Tabs.List variant="filled" color="brand">
    <Tabs.Indicator />
    <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
    <Tabs.Trigger value="activity">Activity</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="overview"><Overview /></Tabs.Content>
  <Tabs.Content value="activity"><Activity /></Tabs.Content>
</Tabs.Root>

Controlled

Drive the value from state. onChange hands you the selected value directly (the data-driven form); the compound Tabs.Root takes Ark's own value / onValueChange.

'use client';
import { useState } from 'react';
import { Tabs } from '@42-central/ui-react/tabs';

export function ProjectTabs() {
  const [tab, setTab] = useState('overview');
  return (
    <Tabs
      value={tab}
      onChange={setTab}
      data={[
        { value: 'overview', label: 'Overview', content: <Overview /> },
        { value: 'activity', label: 'Activity', content: <Activity /> },
      ]}
    />
  );
}

API

Prop

Type

Accessibility

  • Built on Ark UI's Tabs machine: role="tablist" / "tab" / "tabpanel", with aria-selected and roving tabIndex on the triggers and aria-controls / aria-labelledby linking each trigger to its panel.
  • ← / → (or ↑ / ↓ when orientation="vertical") move focus between triggers; activationMode="automatic" (the default) activates the focused tab immediately, "manual" waits for Enter / a click. Tab enters and leaves the tablist as a single stop.
  • A disabled trigger is skipped by arrow-key navigation and unclickable.
  • RTL is honored via the document dir.

Changelog

v0.19.0
  • The default ThemeIcon and the filled Tabs and SegmentGroup tray use the kit's hairline border and quiet fill instead of the gray scale

    #316
  • Right to left, Tabs and SegmentGroup separators sit between the items, and Table headers align to the start with scroll fades on the correct edge

    #331
v0.18.0
  • asChild is a compile error on the data forms of Accordion, Tabs and Breadcrumb, on the ready-made Notification and on AppShell.Sidebar (their Root parts keep it)

    #294
  • The data form's onChange is typed with your tab values only, no longer widened by the native ChangeEvent

    #294
v0.9.2
  • color moves from Tabs.Root to Tabs.List, so it no longer tints panel content

    #143
  • The selected label follows its pill's palette, staying legible on colored strips and while hovered

    #143
v0.7.0
  • The sliding indicator matches SegmentGroup's, and the default variant tints with --c-solid

    #109
v0.5.0
  • New Tabs component, with a sliding pill indicator

    #60

On this page