Tabs

Organize content into switchable panels triggered by a tab bar

Tabs and TabsNavigation share their visual styling but use different semantics. Use TabsNavigation when the controls scroll to page sections or filter an independent result list.

Installation

pnpm add @wandercom/design-system-web

Usage

Use Tabs when each trigger reveals one corresponding content panel. The compound components are built on Base UI's Tabs primitive and provide the expected tab roles, panel relationships, and keyboard behavior.

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

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

Use TabsNavigation when the controls scroll to page sections or filter an independent result list.

Examples

Loading example...

Variants

Default (pill)

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

9 lines
<TabsRoot defaultValue="tab1" size="md" variant="default">
  <TabsList>
    <TabsTrigger value="tab1">Tab 1</TabsTrigger>
    <TabsTrigger value="tab2">Tab 2</TabsTrigger>
    <TabsIndicator />
  </TabsList>
  <TabsContent value="tab1">First panel.</TabsContent>
  <TabsContent value="tab2">Second panel.</TabsContent>
</TabsRoot>

Underline

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

9 lines
<TabsRoot defaultValue="tab1" variant="underline">
  <TabsList bordered>
    <TabsTrigger value="tab1">Tab 1</TabsTrigger>
    <TabsTrigger value="tab2">Tab 2</TabsTrigger>
    <TabsIndicator />
  </TabsList>
  <TabsContent value="tab1">First panel.</TabsContent>
  <TabsContent value="tab2">Second panel.</TabsContent>
</TabsRoot>

Props

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. It renders a sliding bottom border for the underline variant. The default variant uses the active trigger's background and does not render a separate indicator.

className?

string
Additional CSS classes for the animated underline 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.

Legacy high-level API

The high-level Tabs component and TabsProps are deprecated because they render tab semantics without corresponding panels. They remain temporarily available for patch compatibility. Do not use them for new work.

The old API is intentionally omitted from the live examples so the docs do not render an incomplete WAI-ARIA Tabs pattern. It remains documented here to help existing consumers identify and migrate it:

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

<Tabs
  items={[
    { label: "All reviews", value: "all" },
    { label: "Published", value: "published" },
    { label: "Drafts", value: "drafts" },
  ]}
  onChange={setFilter}
  value={filter}
/>

Follow the legacy panel-less Tabs migration guide to move navigation and filtering controls to TabsNavigation. Compound TabsRoot, TabsList, TabsTrigger, TabsContent, and TabsIndicator are not deprecated.

items

Array<{ label: ReactNode; value: string; disabled?: boolean }>
Legacy tab items. Migrate this array to TabsNavigation.

onChange?

(value: string) => void
Legacy callback. Rename to onValueChange when migrating.

classNames?

{ list?: string; trigger?: string }
Legacy slot names. Rename classNames.trigger to classNames.item when migrating.
Tabs