Tabs

Organize content into switchable panels triggered by a tab bar

Installation

pnpm add @wandercom/design-system-web

Usage

The Tabs component provides both a high-level convenience API and low-level compound components for full composition control. Built on Base UI's Tabs primitive.

High-level API

Use the Tabs component for simple tab bars without associated content panels:

import { Tabs } from '@wandercom/design-system-web/ui/tabs';

export function Example() {
  const [selected, setSelected] = useState("all");

  return (
    <Tabs
      items={[
        { label: "All reviews", value: "all" },
        { label: "Published", value: "published" },
        { label: "Drafts", value: "drafts" },
      ]}
      value={selected}
      onChange={setSelected}
    />
  );
}

Compound components

For tab bars with associated content panels, use the individual primitives. Place <TabsIndicator /> inside <TabsList> to get the animated active-tab indicator:

import {
  TabsRoot,
  TabsList,
  TabsTrigger,
  TabsIndicator,
  TabsContent,
} from '@wandercom/design-system-web/ui/tabs';

export function Example() {
  return (
    <TabsRoot defaultValue="tab1">
      <TabsList>
        <TabsTrigger value="tab1">Details</TabsTrigger>
        <TabsTrigger value="tab2">Reviews</TabsTrigger>
        <TabsIndicator />
      </TabsList>
      <TabsContent value="tab1">Property details here.</TabsContent>
      <TabsContent value="tab2">Guest reviews here.</TabsContent>
    </TabsRoot>
  );
}

Examples

Loading example...

Variants

Default (pill)

Pill-shaped tab triggers with a filled background on the active tab. Supports sm and md sizes.

<TabsRoot defaultValue="tab1" variant="default" size="md">
  <TabsList>
    <TabsTrigger value="tab1">Tab 1</TabsTrigger>
    <TabsTrigger value="tab2">Tab 2</TabsTrigger>
    <TabsIndicator />
  </TabsList>
</TabsRoot>

Underline

Borderless tab triggers with an underline on the active tab. Size prop is ignored; height is fixed at 40px. Use the bordered prop on TabsList to add a full-width bottom border.

<TabsRoot defaultValue="tab1" variant="underline">
  <TabsList bordered>
    <TabsTrigger value="tab1">Tab 1</TabsTrigger>
    <TabsTrigger value="tab2">Tab 2</TabsTrigger>
    <TabsIndicator />
  </TabsList>
</TabsRoot>

Props

Tabs

items

Array<{ label: ReactNode; value: string; disabled?: boolean }>
Tab items to render.

variant?

'default' | 'underline'
Visual style variant. Defaults to "default".

size?

'sm' | 'md'
Size for default pill tabs. Ignored for underline variant. Defaults to "md".

bordered?

boolean
Show a bottom border on the tab list. Defaults to false.

value?

string
Controlled active tab value.

defaultValue?

string
Initial active tab value for uncontrolled usage.

onChange?

(value: string) => void
Callback when the active tab changes.

className?

string
Additional CSS classes for the root element.

classNames?

{ list?: string; trigger?: string }
Per-slot className overrides for the tab list and triggers.

TabsRoot

variant?

'default' | 'underline'
Visual style variant passed to child components via context. Defaults to "default".

size?

'sm' | 'md'
Size variant passed to child components via context. Defaults to "md".

defaultValue?

string | number
Initial active tab value for uncontrolled usage.

value?

string | number
Controlled active tab value.

onValueChange?

(value: string | number) => void
Callback when the active tab changes.

orientation?

'horizontal' | 'vertical'
Orientation of the tab list for keyboard navigation. Defaults to "horizontal".

className?

string
Additional CSS classes for the root container.

TabsList

bordered?

boolean
Show a bottom border on the tab list. Useful with the underline variant for a full-width divider. Defaults to false.

className?

string
Additional CSS classes for the tab list container.

TabsTrigger

value

string | number
Unique value identifying this tab. Must match a TabsContent value.

disabled?

boolean
Disables the tab trigger when true.

className?

string
Additional CSS classes for the tab trigger.

TabsContent

value

string | number
Value matching the corresponding TabsTrigger.

className?

string
Additional CSS classes for the content panel.

TabsIndicator

Animated indicator that slides between active tabs. Place it as a child of TabsList. Base UI positions it automatically via CSS custom properties (--active-tab-width, --active-tab-left).

For the default variant, it renders as a pill-shaped background behind the active tab. For the underline variant, it renders as a sliding bottom border.

The high-level Tabs component includes TabsIndicator automatically. When using compound components, add <TabsIndicator /> inside <TabsList> yourself.

className?

string
Additional CSS classes for the animated indicator.

Accessibility

Built on Base UI's Tabs primitive, which follows the WAI-ARIA Tabs pattern.

Keyboard interaction:

  • ArrowLeft / ArrowRight - Navigate between tabs (horizontal orientation)
  • ArrowUp / ArrowDown - Navigate between tabs (vertical orientation)
  • Home - Move focus to the first tab
  • End - Move focus to the last tab
  • Enter / Space - Activate the focused tab

Only the active tab is in the tab sequence (tabIndex={0}). Inactive tabs use tabIndex={-1} so the user can tab past the entire tab list in one keystroke. Each tab is linked to its panel via aria-controls and aria-selected.

Reduced motion: The TabsIndicator slide animation respects prefers-reduced-motion: reduce. When the user's OS preference is set to reduce motion, the indicator transitions are disabled via motion-reduce:transition-none.

Tabs