Navigation

Tabs
v1.0.7
Documentation Under Review

Interactive tab navigation for organizing content into separate views with accessible keyboard navigation and state management.

Tabs

Interactive tab navigation component built on Radix UI primitives with full keyboard navigation support and WCAG 2.1 AA compliance. Organize content into separate views with a clean, accessible tab interface.

Installation

Install the Tabs component from the Catalyst component library:

npm install @pmi/catalyst-tabs

Examples

Basic Usage

Create a simple tabbed interface with multiple content panels:

Preview

Overview

View your account overview, recent activity, and key metrics at a glance.

Disabled State

Disable all tabs to prevent interaction during loading or processing states:

Preview

Content is disabled while processing...

Custom Styling

Customize tab appearance using dual className props with aqua theme:

Preview

Aqua-themed tab content showing custom colors

Controlled Tabs

Use controlled state for programmatic tab switching and state tracking:

Preview
Current tab:personal

Update your personal information and profile details.

Custom Tab Styling

Customize tab list appearance with additional styling:

Preview

Welcome to the home page with custom styled tabs.

Advanced Examples

Data Table with Filters

Use tabs to filter table data by status:

Preview
Order IDCustomerStatusAmount
ORD-001Alice Johnsonpending$129.99
ORD-002Bob Smithcompleted$89.50
ORD-003Carol Whitepending$199.00
ORD-004David Browncompleted$45.00
ORD-005Eve Daviscancelled$75.25

Feature Comparison Table

Compare product features across different plans using tabs:

Preview

Basic

$9/month

  • ✓5 Projects
  • ✓10 GB Storage
  • ✓Email Support
  • ✓Basic Analytics

API Reference

TabsRoot

The root container component that manages tab state and coordination between triggers and content panels.

Prop

Type

TabsList

Container for tab triggers that displays the clickable tab buttons.

Prop

Type

TabsTrigger

Individual tab button that activates its associated content panel.

Prop

Type

TabsContent

Content panel associated with a specific tab trigger.

Prop

Type

Inherited Props

All components inherit additional props from their respective Radix UI primitives:

  • TabsRoot: Inherits from Radix.Tabs.Root
  • TabsList: Inherits from Radix.Tabs.List
  • TabsTrigger: Inherits from Radix.Tabs.Trigger
  • TabsContent: Inherits from Radix.Tabs.Content

See Radix UI Tabs documentation for complete inherited prop details.

Compound Component Pattern

The Tabs component uses a compound component pattern where four related components work together:

<TabsRoot>
  {/* Container managing state */}
  <TabsList>
    {/* Tab buttons container */}
    <TabsTrigger value="tab1">Label</TabsTrigger>
    <TabsTrigger value="tab2">Label</TabsTrigger>
  </TabsList>

  {/* Content panels */}
  <TabsContent value="tab1">Content 1</TabsContent>
  <TabsContent value="tab2">Content 2</TabsContent>
</TabsRoot>

Key Concepts:

  1. Value Association: TabsTrigger and TabsContent are linked via matching value props
  2. State Management: TabsRoot controls which tab is active
  3. Automatic ARIA: Radix UI handles all accessibility attributes
  4. Flexible Layout: Components can be arranged in various layouts

Advanced Patterns

asChild Composition

Use the asChild prop to render custom wrapper elements while preserving tab functionality:

// Custom navigation wrapper for TabsList
<TabsList asChild>
  <nav role="tablist" className="custom-nav">
    <TabsTrigger value="home">Home</TabsTrigger>
    <TabsTrigger value="about">About</TabsTrigger>
  </nav>
</TabsList>

// Custom button wrapper for TabsTrigger
<TabsTrigger asChild value="settings">
  <button className="custom-button">
    <SettingsIcon /> Settings
  </button>
</TabsTrigger>

The asChild prop uses Radix UI's Slot component to merge props with the child element.

Dual className Pattern

TabsTrigger accepts two className props for layered styling:

<TabsTrigger
  value="tab1"
  className="border-styles layout-styles" // Outer container
  classNameInner="text-styles background-styles" // Inner wrapper
>
  Tab Label
</TabsTrigger>

Styling Layers:

  • className: Controls border, layout, and active state indicators
  • classNameInner: Controls text, background, hover, and focus styles

Example - Aqua Theme:

<TabsTrigger
  value="tab1"
  className="data-[state=active]:border-b-pmi-aqua-500 data-[state=inactive]:border-b-neutral-300"
  classNameInner="group-hover:bg-pmi-aqua-100 group-focus-visible:ring-pmi-aqua-500"
>
  Aqua Theme Tab
</TabsTrigger>

Controlled State Management

Use controlled state for complex logic, form integration, or URL synchronization:

const [activeTab, setActiveTab] = useState('overview');

// Programmatically change tabs
const navigateToSettings = () => setActiveTab('settings');

// Track tab history
const [history, setHistory] = useState<string[]>([]);
const handleTabChange = (value: string) => {
  setActiveTab(value);
  setHistory([...history, value]);
};

<TabsRoot value={activeTab} onValueChange={handleTabChange}>
  {/* tabs */}
</TabsRoot>;

Uncontrolled State (Simpler)

Use uncontrolled state with defaultValue when you don't need programmatic control:

<TabsRoot defaultValue="overview">{/* tabs - Radix UI manages state internally */}</TabsRoot>

Accessibility

The Tabs component is built on Radix UI primitives and provides comprehensive keyboard navigation and screen reader support, meeting WCAG 2.1 AA standards.

Keyboard Navigation

KeyAction
TabMove focus to the active tab or first tab
Arrow LeftActivate and focus the previous tab (horizontal)
Arrow RightActivate and focus the next tab (horizontal)
Arrow UpActivate and focus the previous tab (vertical)
Arrow DownActivate and focus the next tab (vertical)
HomeActivate and focus the first tab
EndActivate and focus the last tab
Enter / SpaceActivate focused tab (when activationMode="manual")

Automatic vs Manual Activation:

  • Automatic (default): Arrow keys immediately activate tabs
  • Manual: Arrow keys focus tabs, Enter/Space activates them

Screen Reader Support

The component provides full screen reader support with automatic ARIA attribute management:

Tab States Announced:

  • "Tab [name], selected" - Active tab
  • "Tab [name]" - Inactive tab
  • "Tab [name], disabled" - Disabled tab
  • "[number] of [total] tabs" - Position information

Content Panels:

  • Associated with trigger via aria-labelledby
  • Only visible panel announced
  • Smooth transitions between panels

ARIA Attributes

Radix UI automatically manages all required ARIA attributes:

TabsList:

  • role="tablist" - Identifies the tab list container
  • aria-orientation="horizontal" or "vertical" - Indicates tab orientation

TabsTrigger:

  • role="tab" - Identifies each tab button
  • aria-selected="true" or "false" - Indicates active state
  • aria-controls="[content-id]" - Associates trigger with content
  • aria-disabled="true" - When disabled prop is true
  • tabindex="0" (active) or "-1" (inactive) - Focus management

TabsContent:

  • role="tabpanel" - Identifies the content panel
  • aria-labelledby="[trigger-id]" - Associates content with trigger
  • hidden - Hides inactive panels from assistive technologies

Focus Management

Visible Focus Indicators:

  • 2px solid inset ring on focus: ring-2 ring-inset
  • Default color: ring-pmi-off-black-800 (light), ring-white (dark)
  • Custom themes can override: group-focus-visible:ring-[color]

Focus Behavior:

  • Only one tab trigger is focusable at a time (roving tabindex)
  • Focus automatically moves when arrow keys change active tab
  • Focus persists on tab trigger, not content panel
  • :focus-visible used to show focus only for keyboard navigation

WCAG Compliance:

  • Focus indicators meet 3:1 contrast ratio requirement
  • Focus visible on all interactive elements
  • No focus traps or keyboard navigation issues

Color Contrast

All text and interactive elements meet WCAG 2.1 AA contrast requirements:

Text Contrast:

  • Inactive tabs: text-pmi-off-black-600 (≥4.5:1 contrast)
  • Active tabs: text-pmi-off-black-800 (≥7:1 contrast)
  • Dark mode inactive: text-pmi-off-black-200 (≥4.5:1 contrast)
  • Dark mode active: text-white (≥7:1 contrast)

Border Contrast:

  • Active border: 2px border-b-pmi-off-black-800 (≥3:1 contrast)
  • Inactive border: 1px border-b-pmi-neutral-200 (≥3:1 contrast)
  • Dark mode active: border-b-white (≥3:1 contrast)

Disabled State:

  • 40% opacity: opacity-40
  • Maintains sufficient contrast for visibility
  • Pointer events disabled: pointer-events-none

Best Practices

  1. Always Provide Labels: Ensure every tab trigger has clear, descriptive text
  2. Unique Values: Use unique value props for each tab trigger/content pair
  3. Logical Order: Arrange tabs in a logical, predictable order
  4. Visible Content: Ensure tab content is visible and not cut off
  5. Loading States: Show loading indicators when switching to tabs with async content
  6. Error Handling: Display error messages within TabsContent when data fails to load
  7. Mobile Consideration: Test tab overflow on small screens, consider horizontal scroll or dropdown

Design Tokens

The Tabs component uses Catalyst design tokens for consistent theming across light and dark modes.

Colors

Border Colors:

// Active state (2px border)
border - b - pmi - off - black - 800; // Light mode active
dark: border - b - white; // Dark mode active

// Inactive state (1px border)
border - b - pmi - neutral - 200; // Light mode inactive
dark: border - b - pmi - off - black - 600; // Dark mode inactive

Text Colors:

// Inactive text
text-pmi-off-black-600             // Light mode inactive
dark:text-pmi-off-black-200        // Dark mode inactive

// Active text
group-data-[state=active]:text-pmi-off-black-800  // Light mode active
dark:group-data-[state=active]:text-white         // Dark mode active

Hover Colors:

group-hover:bg-pmi-violet-100      // Light mode hover background
dark:group-hover:bg-pmi-violet-600 // Dark mode hover background

Focus Colors:

group-focus-visible:ring-pmi-off-black-800  // Light mode focus ring
dark:group-focus-visible:ring-white         // Dark mode focus ring

Custom Theme - Aqua

Product variant includes an aqua theme example:

// Aqua theme border colors
className =
  'data-[state=active]:border-b-pmi-aqua-500 data-[state=inactive]:border-b-neutral-300 dark:data-[state=active]:border-b-pmi-aqua-500 dark:data-[state=inactive]:border-b-neutral-700';

// Aqua theme hover/focus colors
classNameInner =
  'group-hover:bg-pmi-aqua-100 dark:group-hover:bg-pmi-aqua-700 group-focus-visible:ring-pmi-aqua-500 dark:group-focus-visible:ring-pmi-aqua-500';

Spacing

Padding:

px-1.5 py-1      // TabsTrigger outer padding (6px horizontal, 4px vertical)
p-1              // TabsTrigger inner padding (4px all sides)

Border Width:

border - b; // 1px inactive border
border - b - 2; // 2px active border

Focus Ring:

ring - 2; // 2px ring width
ring - inset; // Inset ring positioning

Transitions

transition; // Smooth transitions for all state changes

All color, border, and background changes use CSS transitions for smooth visual feedback.

See Also

  • Form - Multi-step forms with tab navigation
  • Card - Common container for tab content panels
  • Button - Custom tab trigger styling
  • Badge - Tab labels with notification counts
  • Accordion - Alternative vertical navigation pattern
  • Tabs - Marketing variant with same functionality

Component Status: Stable
Package: @pmi/[email protected]
Radix Primitive: @radix-ui/[email protected]

On this page