Light

Tabs

A set of layered sections of content—known as tab panels—that are displayed one at a time.

Spec · from metadata

When to use

  • Organizing related content into switchable panels
  • Presenting alternative views of the same data set
  • Navigation within a section (not primary site navigation)

When not to use

  • For primary app navigation — use a NavBar or Sidebar instead
  • For step-by-step flows — use a Stepper or multi-step form instead
  • For collapsible sections — use Accordion instead

Variants

PropValuesDefaultDescription
variantdefaultlinedefaultTabsList variant: 'default' renders pill-style with bg-muted background; 'line' renders underline-style with transparent background

Anti-patterns

Avoid<Tabs><TabsList variant="line">...</TabsList></Tabs>
Prefer<Tabs><TabsList variant="line">...</TabsList></Tabs>

This is actually correct — just ensure variant is on TabsList, not on Tabs root

Avoid<Tabs><TabsTrigger>Tab 1</TabsTrigger><TabsContent>Content</TabsContent></Tabs>
Prefer<Tabs defaultValue="tab1"><TabsList><TabsTrigger value="tab1">Tab 1</TabsTrigger></TabsList><TabsContent value="tab1">Content</TabsContent></Tabs>

TabsTrigger must be wrapped in TabsList, and both trigger and content need matching value props

Accessibility

  • Required ARIAaria-selected (managed by Radix)aria-controls (managed by Radix)
  • Screen readerRadix handles aria-selected, aria-controls, and role attributes automatically

Token bindings

TokenCategoryUsage
bg-mutedcolorDefault variant TabsList background
text-muted-foregroundcolorInactive trigger text color
bg-cardcolorActive trigger background (default variant)
text-foregroundcolorActive trigger text and line indicator color

Import

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@timelycare/helix-ui"

Props

interface TabsProps {
  defaultValue?: string
  value?: string
  onValueChange?: (value: string) => void
  orientation?: "horizontal" | "vertical"
  dir?: "ltr" | "rtl"
  className?: string
  children: React.ReactNode
}

interface TabsListProps {
  variant?: "default" | "line"
  className?: string
  children: React.ReactNode
}

interface TabsTriggerProps {
  value: string
  disabled?: boolean
  className?: string
  children: React.ReactNode
}

Variants

Default (Pill / Segment)

The default variant renders tabs inside a muted background container. The active tab appears as a raised pill with a white background and subtle shadow.

PartTailwind
TabsListh-9 bg-muted p-[3px] rounded-lg flex items-center gap-0.5
TabsTriggerflex-1 px-3 py-1 rounded-md text-sm font-medium text-muted-foreground
TabsTrigger (active)bg-background text-foreground shadow-sm
TabsTrigger (inactive)bg-transparent text-muted-foreground
TabsContentmt-2
<Tabs defaultValue="overview">
  <TabsList>
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="analytics">Analytics</TabsTrigger>
    <TabsTrigger value="reports">Reports</TabsTrigger>
    <TabsTrigger value="settings">Settings</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">Overview content</TabsContent>
  <TabsContent value="analytics">Analytics content</TabsContent>
</Tabs>

Line

Use variant="line" on TabsList for an underline indicator instead of a pill background. The list has a bottom border and the active tab shows a colored underline.

PartTailwind
TabsListborder-b border-border flex bg-transparent p-0
TabsTriggerpx-4 py-2 text-sm font-medium text-muted-foreground relative
TabsTrigger (active)text-foreground + bottom border indicator (2px primary)
TabsTrigger (inactive)text-muted-foreground
<Tabs defaultValue="overview">
  <TabsList variant="line">
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="analytics">Analytics</TabsTrigger>
    <TabsTrigger value="reports">Reports</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">Overview content</TabsContent>
</Tabs>

Orientation

Horizontal (default)

Tabs are laid out in a horizontal row. This is the default behavior.

Vertical

Use orientation="vertical" on the Tabs root for a vertical layout. The tabs list stacks vertically and the content panel appears beside it.

PartTailwind
TabsList (vertical, default)flex-col items-stretch h-auto bg-muted p-[3px] rounded-lg gap-0.5
TabsTrigger (vertical)px-3 py-1.5 rounded-md text-left
<Tabs defaultValue="account" orientation="vertical">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
    <TabsTrigger value="notifications">Notifications</TabsTrigger>
  </TabsList>
  <TabsContent value="account">Account settings here.</TabsContent>
  <TabsContent value="password">Password settings here.</TabsContent>
</Tabs>

States

StateStyling
Active (default)bg-background shadow-sm text-foreground
Active (line)text-foreground + 2px primary underline
Inactivebg-transparent text-muted-foreground
HoverSubtle text color shift toward foreground
Focusring-[3px] ring-ring/50
Disabledtext-muted-foreground opacity-50 pointer-events-none

Icons

Size: size-4 (16px) Spacing: Automatic gap-2 (8px between icon and text) when icon is a direct child of TabsTrigger

<TabsTrigger value="preview">
  <AppWindowIcon />
  Preview
</TabsTrigger>
<TabsTrigger value="code">
  <CodeIcon />
  Code
</TabsTrigger>

Icons work with both the default and line variants.


Badge

Style: h-5 min-w-5 px-1 rounded-full bg-primary text-xs font-bold text-primary-foreground

<TabsTrigger value="notifications">
  Notifications
  <Badge className="h-5 min-w-5 rounded-full">8</Badge>
</TabsTrigger>

Common Patterns

Basic Tabs (Default Variant)

<Tabs defaultValue="account">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">Account settings here.</TabsContent>
  <TabsContent value="password">Password settings here.</TabsContent>
</Tabs>

Line Tabs

<Tabs defaultValue="overview">
  <TabsList variant="line">
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="analytics">Analytics</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">Overview content</TabsContent>
  <TabsContent value="analytics">Analytics content</TabsContent>
</Tabs>

Vertical Tabs

<Tabs defaultValue="account" orientation="vertical" className="flex gap-4">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
    <TabsTrigger value="notifications">Notifications</TabsTrigger>
  </TabsList>
  <div className="flex-1">
    <TabsContent value="account">Account content</TabsContent>
    <TabsContent value="password">Password content</TabsContent>
    <TabsContent value="notifications">Notifications content</TabsContent>
  </div>
</Tabs>

With Icons

<Tabs defaultValue="preview">
  <TabsList>
    <TabsTrigger value="preview">
      <AppWindowIcon />
      Preview
    </TabsTrigger>
    <TabsTrigger value="code">
      <CodeIcon />
      Code
    </TabsTrigger>
  </TabsList>
  <TabsContent value="preview">Preview content</TabsContent>
  <TabsContent value="code">Code content</TabsContent>
</Tabs>

With Badge

<Tabs defaultValue="billing">
  <TabsList>
    <TabsTrigger value="billing">Billing</TabsTrigger>
    <TabsTrigger value="notifications">
      Notifications
      <Badge className="h-5 min-w-5 rounded-full">8</Badge>
    </TabsTrigger>
  </TabsList>
</Tabs>

With Disabled Tab

<Tabs defaultValue="home">
  <TabsList>
    <TabsTrigger value="home">Home</TabsTrigger>
    <TabsTrigger value="settings" disabled>Settings</TabsTrigger>
  </TabsList>
  <TabsContent value="home">Home content</TabsContent>
</Tabs>

Tabs with Cards

Content sections organized by tabs, with each panel containing a Card.

<Tabs defaultValue="overview">
  <TabsList>
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="analytics">Analytics</TabsTrigger>
    <TabsTrigger value="settings">Settings</TabsTrigger>
  </TabsList>
  <TabsContent value="overview" className="space-y-4">
    <Card>
      <CardHeader>
        <CardTitle>Overview</CardTitle>
      </CardHeader>
      <CardContent>
        {/* Content */}
      </CardContent>
    </Card>
  </TabsContent>
  {/* Other tab contents */}
</Tabs>

Spacing

  • TabsList to TabsContent: mt-2 (built into TabsContent)
  • Cards within tabs: space-y-4

Controlled Tabs

const [activeTab, setActiveTab] = useState("account")

<Tabs value={activeTab} onValueChange={setActiveTab}>
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
</Tabs>

Variant Comparison

FeatureDefault (Pill)Line
Background containerbg-muted rounded-lgTransparent with border-b
Active indicatorWhite pill with shadow2px underline in primary color
Supports verticalYesYes
Supports iconsYesYes
Supports badgesYesYes
Best forStandalone sections, settingsPage-level navigation, content areas

Accessibility

  • Built on Radix Tabs — handles keyboard and focus automatically
  • Keyboard: Arrow Left/Right between horizontal tabs, Arrow Up/Down for vertical tabs, Tab to enter panel content
  • Screen reader: announces tab role, selected state, and panel association
  • Uses tabpanel role with aria-labelledby linking to tab trigger
  • Automatic focus management with roving tabindex
  • Supports dir="rtl" for right-to-left languages

Gotchas

ProblemSolution
Content not showingEnsure value matches defaultValue or controlled value
Tabs not filling widthTabsTrigger uses flex-1 by default in the default variant
Icon alignmentIcons are auto-spaced via gap-2 — no extra class needed
Badge spacingPlace Badge directly inside TabsTrigger — gap is automatic
Want underline style, not pillUse variant="line" on TabsList
Vertical not workingAdd orientation="vertical" on the Tabs root, not TabsList
Arrow keys go wrong direction in verticalRadix automatically switches to Up/Down arrows when vertical

See Also


Last updated: February 20, 2026