Tabs

A set of layered sections of content — known as tab panels — that are displayed one at a time. Built on Radix UI Tabs for full keyboard navigation and accessibility.

Installation

npm install @designforge/ui

Usage

import { Tabs, TabsList, TabsTrigger, TabsContent } from "@designforge/ui";
 
export default function Example() {
  return (
    <Tabs defaultValue="account">
      <TabsList>
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
      </TabsList>
      <TabsContent value="account">Account settings go here.</TabsContent>
      <TabsContent value="password">Password settings go here.</TabsContent>
    </Tabs>
  );
}

Examples

Basic

The simplest usage with defaultValue to set the initially active tab.

import { Tabs, TabsList, TabsTrigger, TabsContent } from "@designforge/ui";
 
export default function BasicTabs() {
  return (
    <Tabs defaultValue="overview">
      <TabsList>
        <TabsTrigger value="overview">Overview</TabsTrigger>
        <TabsTrigger value="analytics">Analytics</TabsTrigger>
        <TabsTrigger value="reports">Reports</TabsTrigger>
      </TabsList>
      <TabsContent value="overview">
        <p>Overview content panel.</p>
      </TabsContent>
      <TabsContent value="analytics">
        <p>Analytics content panel.</p>
      </TabsContent>
      <TabsContent value="reports">
        <p>Reports content panel.</p>
      </TabsContent>
    </Tabs>
  );
}

Controlled

Use value and onValueChange to control the active tab from external state.

import { useState } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@designforge/ui";
 
export default function ControlledTabs() {
  const [activeTab, setActiveTab] = useState("profile");
 
  return (
    <div>
      <p className="mb-4 text-sm text-muted-foreground">
        Active tab: <strong>{activeTab}</strong>
      </p>
      <Tabs value={activeTab} onValueChange={setActiveTab}>
        <TabsList>
          <TabsTrigger value="profile">Profile</TabsTrigger>
          <TabsTrigger value="billing">Billing</TabsTrigger>
          <TabsTrigger value="notifications">Notifications</TabsTrigger>
        </TabsList>
        <TabsContent value="profile">
          <p>Manage your profile details.</p>
        </TabsContent>
        <TabsContent value="billing">
          <p>Update your billing information.</p>
        </TabsContent>
        <TabsContent value="notifications">
          <p>Configure notification preferences.</p>
        </TabsContent>
      </Tabs>
    </div>
  );
}

Vertical

Set orientation="vertical" to stack the tab list and content side by side. Update your layout with flex to position them correctly.

import { Tabs, TabsList, TabsTrigger, TabsContent } from "@designforge/ui";
 
export default function VerticalTabs() {
  return (
    <Tabs defaultValue="general" orientation="vertical" className="flex gap-6">
      <TabsList className="flex-col h-auto">
        <TabsTrigger value="general">General</TabsTrigger>
        <TabsTrigger value="security">Security</TabsTrigger>
        <TabsTrigger value="integrations">Integrations</TabsTrigger>
        <TabsTrigger value="advanced">Advanced</TabsTrigger>
      </TabsList>
      <div className="flex-1">
        <TabsContent value="general">General settings.</TabsContent>
        <TabsContent value="security">Security settings.</TabsContent>
        <TabsContent value="integrations">Third-party integrations.</TabsContent>
        <TabsContent value="advanced">Advanced configuration.</TabsContent>
      </div>
    </Tabs>
  );
}

Disabled Tab

Pass disabled on a TabsTrigger to prevent interaction. The trigger is visually dimmed and skipped during keyboard navigation.

import { Tabs, TabsList, TabsTrigger, TabsContent } from "@designforge/ui";
 
export default function DisabledTab() {
  return (
    <Tabs defaultValue="active">
      <TabsList>
        <TabsTrigger value="active">Active</TabsTrigger>
        <TabsTrigger value="unavailable" disabled>
          Unavailable
        </TabsTrigger>
        <TabsTrigger value="another">Another</TabsTrigger>
      </TabsList>
      <TabsContent value="active">This tab is active and accessible.</TabsContent>
      <TabsContent value="unavailable">This content cannot be reached.</TabsContent>
      <TabsContent value="another">Another accessible tab.</TabsContent>
    </Tabs>
  );
}

With Icons

Combine icons with labels inside TabsTrigger for a richer visual hierarchy.

import { LayoutDashboard, BarChart2, FileText, Settings } from "lucide-react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@designforge/ui";
 
export default function TabsWithIcons() {
  return (
    <Tabs defaultValue="dashboard">
      <TabsList>
        <TabsTrigger value="dashboard">
          <LayoutDashboard className="mr-2 h-4 w-4" />
          Dashboard
        </TabsTrigger>
        <TabsTrigger value="analytics">
          <BarChart2 className="mr-2 h-4 w-4" />
          Analytics
        </TabsTrigger>
        <TabsTrigger value="reports">
          <FileText className="mr-2 h-4 w-4" />
          Reports
        </TabsTrigger>
        <TabsTrigger value="settings">
          <Settings className="mr-2 h-4 w-4" />
          Settings
        </TabsTrigger>
      </TabsList>
      <TabsContent value="dashboard">Dashboard view.</TabsContent>
      <TabsContent value="analytics">Analytics view.</TabsContent>
      <TabsContent value="reports">Reports view.</TabsContent>
      <TabsContent value="settings">Settings view.</TabsContent>
    </Tabs>
  );
}

Navigation-Style Layout

Override the default pill style to create an underline tab bar — common for settings pages and top-level navigation.

import { Tabs, TabsList, TabsTrigger, TabsContent } from "@designforge/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@designforge/ui";
 
export default function NavigationStyleTabs() {
  return (
    <div className="max-w-2xl mx-auto">
      <h1 className="text-2xl font-bold mb-6">Account Settings</h1>
      <Tabs defaultValue="profile">
        <TabsList className="w-full justify-start border-b rounded-none bg-transparent px-0 mb-6">
          <TabsTrigger
            value="profile"
            className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent shadow-none"
          >
            Profile
          </TabsTrigger>
          <TabsTrigger
            value="security"
            className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent shadow-none"
          >
            Security
          </TabsTrigger>
          <TabsTrigger
            value="team"
            className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent shadow-none"
          >
            Team
          </TabsTrigger>
        </TabsList>
        <TabsContent value="profile">
          <Card>
            <CardHeader>
              <CardTitle>Profile Information</CardTitle>
            </CardHeader>
            <CardContent>Edit your public profile details here.</CardContent>
          </Card>
        </TabsContent>
        <TabsContent value="security">
          <Card>
            <CardHeader>
              <CardTitle>Security Settings</CardTitle>
            </CardHeader>
            <CardContent>Manage passwords and two-factor authentication.</CardContent>
          </Card>
        </TabsContent>
        <TabsContent value="team">
          <Card>
            <CardHeader>
              <CardTitle>Team Members</CardTitle>
            </CardHeader>
            <CardContent>Invite and manage team members.</CardContent>
          </Card>
        </TabsContent>
      </Tabs>
    </div>
  );
}

Lazy Loaded Content

Track which tabs have been visited and only mount their content after the first activation. This avoids rendering expensive components until they are needed.

import { useState, Suspense, lazy } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@designforge/ui";
 
const HeavyChart = lazy(() => import("./HeavyChart"));
 
export default function LazyTabs() {
  const [visited, setVisited] = useState<Set<string>>(new Set(["summary"]));
 
  const handleChange = (value: string) => {
    setVisited((prev) => new Set([...prev, value]));
  };
 
  return (
    <Tabs defaultValue="summary" onValueChange={handleChange}>
      <TabsList>
        <TabsTrigger value="summary">Summary</TabsTrigger>
        <TabsTrigger value="chart">Chart</TabsTrigger>
        <TabsTrigger value="raw">Raw Data</TabsTrigger>
      </TabsList>
      <TabsContent value="summary">
        <p>Summary statistics rendered immediately.</p>
      </TabsContent>
      <TabsContent value="chart">
        {visited.has("chart") && (
          <Suspense fallback={<p>Loading chart...</p>}>
            <HeavyChart />
          </Suspense>
        )}
      </TabsContent>
      <TabsContent value="raw">
        {visited.has("raw") && <p>Raw data table rendered on first visit.</p>}
      </TabsContent>
    </Tabs>
  );
}

API Reference

Tabs

The root container that manages active tab state. Accepts all div props in addition to the following.

PropTypeDefaultDescription
defaultValuestringThe tab active on initial render (uncontrolled).
valuestringThe controlled active tab value.
onValueChange(value: string) => voidCallback invoked when the active tab changes.
orientation"horizontal" | "vertical""horizontal"The orientation of the tab list; affects arrow key direction.
classNamestringAdditional class names applied to the root element.

TabsList

The container for all TabsTrigger elements. Renders with a muted background and handles roving tabindex for keyboard navigation.

PropTypeDefaultDescription
classNamestringAdditional class names for the list container.

TabsTrigger

An individual tab button. The active trigger receives a white background and a subtle box shadow.

PropTypeDefaultDescription
valuestringRequired. The value this trigger corresponds to.
disabledbooleanfalsePrevents the tab from being selected.
classNamestringAdditional class names for the trigger button.

TabsContent

The panel shown when its corresponding trigger is active. Has a top margin and a visible focus ring.

PropTypeDefaultDescription
valuestringRequired. Must match the trigger's value.
classNamestringAdditional class names for the content panel.

Accessibility

  • Built on Radix UI Tabs, which fully implements the ARIA Tabs pattern.
  • TabsList receives role="tablist", each TabsTrigger receives role="tab", and each TabsContent receives role="tabpanel".
  • aria-selected, aria-controls, and aria-labelledby relationships are managed automatically.
  • Keyboard navigation: Arrow keys move focus between tabs; Home / End jump to the first or last tab. Tab moves focus into the active panel.
  • Disabled tabs receive aria-disabled="true" and are excluded from keyboard roving.
  • When using orientation="vertical", Up/Down arrow keys are used instead of Left/Right so the interaction matches the visual layout.

Live View

Open in Storybook ↗