- Alert
- Avatar
- Badge
- Breadcrumb
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Checkbox Group
- ComboBox
- Command
- Data Table
- Date Field
- Date Picker
- Date Range Picker
- Dialog
- Disclosure
- Disclosure Group
- Drawer
- Empty
- Field
- File Trigger
- Form
- Grid List
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- ListBox
- Menu
- Number Field
- Pagination
- Popover
- Progress Bar
- Radio Group
- Range Calendar
- Resizable
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Switch
- Table
- Tabs
- Tag Group
- Text Field
- Textarea
- Time Field
- Toast
- Toggle Button
- Toggle Button Group
- Tooltip
- Tree
- Typography
Tabs organize content into multiple sections and allow users to navigate between them.
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
TabPanel,
Tabs,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
export function TabsDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-6">
<Tabs defaultSelectedKey="account">
<TabsList aria-label="Account settings">
<TabsTrigger id="account">Account</TabsTrigger>
<TabsTrigger id="password">Password</TabsTrigger>
</TabsList>
<TabPanel id="account">
<Card>
<CardHeader>
<CardTitle>Account</CardTitle>
<CardDescription>
Make changes to your account here. Click save when you're
done.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="tabs-demo-name">Name</Label>
<Input id="tabs-demo-name" defaultValue="Pedro Duarte" />
</div>
<div className="grid gap-3">
<Label htmlFor="tabs-demo-username">Username</Label>
<Input id="tabs-demo-username" defaultValue="@peduarte" />
</div>
</CardContent>
<CardFooter>
<Button>Save changes</Button>
</CardFooter>
</Card>
</TabPanel>
<TabPanel id="password">
<Card>
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>
Change your password here. After saving, you'll be logged
out.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="tabs-demo-current">Current password</Label>
<Input id="tabs-demo-current" type="password" />
</div>
<div className="grid gap-3">
<Label htmlFor="tabs-demo-new">New password</Label>
<Input id="tabs-demo-new" type="password" />
</div>
</CardContent>
<CardFooter>
<Button>Save password</Button>
</CardFooter>
</Card>
</TabPanel>
</Tabs>
</div>
)
}
Installation
pnpmnpmyarnbunpnpm dlx shadcn@latest add tabs
Usage
import { Tabs, TabPanel, TabsList, TabsTrigger } from "@/components/ui/tabs"<Tabs defaultSelectedKey="account">
<TabsList aria-label="Account settings">
<TabsTrigger id="account">Account</TabsTrigger>
<TabsTrigger id="password">Password</TabsTrigger>
</TabsList>
<TabPanel id="account">
Make changes to your account here.
</TabPanel>
<TabPanel id="password">
Change your password here.
</TabPanel>
</Tabs>Examples
Disabled
General settings and preferences.
import {
TabPanel,
Tabs,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
export function TabsDisabled() {
return (
<Tabs defaultSelectedKey="general">
<TabsList aria-label="Settings">
<TabsTrigger id="general">General</TabsTrigger>
<TabsTrigger id="billing" isDisabled>
Billing
</TabsTrigger>
<TabsTrigger id="team">Team</TabsTrigger>
</TabsList>
<TabPanel id="general">
<p className="text-muted-foreground text-sm">
General settings and preferences.
</p>
</TabPanel>
<TabPanel id="billing">
<p className="text-muted-foreground text-sm">
Billing information and subscription details.
</p>
</TabPanel>
<TabPanel id="team">
<p className="text-muted-foreground text-sm">
Manage your team members and their permissions.
</p>
</TabPanel>
</Tabs>
)
}
<Tabs defaultSelectedKey="general">
<TabsList aria-label="Settings">
<TabsTrigger id="general">General</TabsTrigger>
<TabsTrigger id="billing" isDisabled>
Billing
</TabsTrigger>
<TabsTrigger id="team">Team</TabsTrigger>
</TabsList>
<TabPanel id="general">General settings</TabPanel>
<TabPanel id="billing">Billing information</TabPanel>
<TabPanel id="team">Team management</TabPanel>
</Tabs>With Icons
Manage your account settings and preferences.
import { BellIcon, LockIcon, UserIcon } from "lucide-react"
import {
TabPanel,
Tabs,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
export function TabsIcons() {
return (
<Tabs defaultSelectedKey="account">
<TabsList aria-label="Settings">
<TabsTrigger id="account">
<UserIcon />
Account
</TabsTrigger>
<TabsTrigger id="security">
<LockIcon />
Security
</TabsTrigger>
<TabsTrigger id="notifications">
<BellIcon />
Notifications
</TabsTrigger>
</TabsList>
<TabPanel id="account">
<p className="text-muted-foreground text-sm">
Manage your account settings and preferences.
</p>
</TabPanel>
<TabPanel id="security">
<p className="text-muted-foreground text-sm">
Configure your security and privacy settings.
</p>
</TabPanel>
<TabPanel id="notifications">
<p className="text-muted-foreground text-sm">
Choose how you want to receive notifications.
</p>
</TabPanel>
</Tabs>
)
}
import { UserIcon, LockIcon, BellIcon } from "lucide-react"
<Tabs defaultSelectedKey="account">
<TabsList aria-label="Settings">
<TabsTrigger id="account">
<UserIcon />
Account
</TabsTrigger>
<TabsTrigger id="security">
<LockIcon />
Security
</TabsTrigger>
<TabsTrigger id="notifications">
<BellIcon />
Notifications
</TabsTrigger>
</TabsList>
<TabPanel id="account">Account settings</TabPanel>
<TabPanel id="security">Security settings</TabPanel>
<TabPanel id="notifications">Notification preferences</TabPanel>
</Tabs>Orientation
Profile
Manage your profile information, avatar, and personal details.
import {
TabPanel,
Tabs,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
export function TabsOrientation() {
return (
<Tabs defaultSelectedKey="profile" orientation="vertical">
<TabsList aria-label="User settings">
<TabsTrigger id="profile">Profile</TabsTrigger>
<TabsTrigger id="security">Security</TabsTrigger>
<TabsTrigger id="notifications">Notifications</TabsTrigger>
</TabsList>
<TabPanel id="profile">
<div className="space-y-2">
<h3 className="text-lg font-medium">Profile</h3>
<p className="text-muted-foreground text-sm">
Manage your profile information, avatar, and personal details.
</p>
</div>
</TabPanel>
<TabPanel id="security">
<div className="space-y-2">
<h3 className="text-lg font-medium">Security</h3>
<p className="text-muted-foreground text-sm">
Update your password, enable two-factor authentication, and manage
security settings.
</p>
</div>
</TabPanel>
<TabPanel id="notifications">
<div className="space-y-2">
<h3 className="text-lg font-medium">Notifications</h3>
<p className="text-muted-foreground text-sm">
Configure how you receive notifications and updates.
</p>
</div>
</TabPanel>
</Tabs>
)
}
<Tabs defaultSelectedKey="profile" orientation="vertical">
<TabsList aria-label="User settings">
<TabsTrigger id="profile">Profile</TabsTrigger>
<TabsTrigger id="security">Security</TabsTrigger>
<TabsTrigger id="notifications">Notifications</TabsTrigger>
</TabsList>
<TabPanel id="profile">Profile information</TabPanel>
<TabPanel id="security">Security settings</TabPanel>
<TabPanel id="notifications">Notification settings</TabPanel>
</Tabs>Controlled
Overview of your dashboard with key metrics.
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
TabPanel,
Tabs,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
export function TabsControlled() {
const [selectedTab, setSelectedTab] = useState("overview")
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onPress={() => setSelectedTab("overview")}
>
Go to Overview
</Button>
<Button
variant="outline"
size="sm"
onPress={() => setSelectedTab("analytics")}
>
Go to Analytics
</Button>
</div>
<Tabs
selectedKey={selectedTab}
onSelectionChange={(key) => setSelectedTab(key as string)}
>
<TabsList aria-label="Dashboard sections">
<TabsTrigger id="overview">Overview</TabsTrigger>
<TabsTrigger id="analytics">Analytics</TabsTrigger>
<TabsTrigger id="reports">Reports</TabsTrigger>
</TabsList>
<TabPanel id="overview">
<p className="text-muted-foreground text-sm">
Overview of your dashboard with key metrics.
</p>
</TabPanel>
<TabPanel id="analytics">
<p className="text-muted-foreground text-sm">
Detailed analytics and insights.
</p>
</TabPanel>
<TabPanel id="reports">
<p className="text-muted-foreground text-sm">
Generate and view reports.
</p>
</TabPanel>
</Tabs>
</div>
)
}
"use client"
import { useState } from "react"
export default function TabsControlled() {
const [selectedTab, setSelectedTab] = useState("overview")
return (
<Tabs
selectedKey={selectedTab}
onSelectionChange={(key) => setSelectedTab(key as string)}
>
<TabsList aria-label="Dashboard">
<TabsTrigger id="overview">Overview</TabsTrigger>
<TabsTrigger id="analytics">Analytics</TabsTrigger>
<TabsTrigger id="reports">Reports</TabsTrigger>
</TabsList>
<TabPanel id="overview">Overview content</TabPanel>
<TabPanel id="analytics">Analytics content</TabPanel>
<TabPanel id="reports">Reports content</TabPanel>
</Tabs>
)
}Using Menu Components Inside Tabs
When placing a Menu component as a sibling to TabsList (for example, in a toolbar), you need to use createHideableComponent to prevent React Aria's collection system from incorrectly treating the MenuItem components as part of the Tabs collection.
The Problem
React Aria's Tabs component uses a collection system to manage its children (TabTriggers). When you place a Menu inside the Tabs tree, the collection system can incorrectly process MenuItems as if they were TabTriggers, causing errors like:
TypeError: Cannot read properties of null (reading 'isDisabled')
See the related GitHub issue for more details.
The Solution
Wrap your Menu component using createHideableComponent from @react-aria/collections:
"use client"
import { useMemo } from "react"
import { createHideableComponent } from "@react-aria/collections"
import { MenuTrigger, Menu, MenuItem } from "@/components/ui/menu"
import { Button } from "@/components/ui/button"
import { Tabs, TabsList, TabsTrigger, TabPanel } from "@/components/ui/tabs"
export function TabsWithMenu() {
// Create a hideable component to prevent Menu from being part of Tabs collection
const SafeMenu = useMemo(
() =>
createHideableComponent(function () {
return (
<MenuTrigger>
<Button variant="outline">Options</Button>
<Menu>
<MenuItem id="edit">Edit</MenuItem>
<MenuItem id="delete">Delete</MenuItem>
</Menu>
</MenuTrigger>
)
}),
[]
)
return (
<Tabs defaultSelectedKey="account">
<div className="flex items-center justify-between">
<TabsList aria-label="Settings">
<TabsTrigger id="account">Account</TabsTrigger>
<TabsTrigger id="security">Security</TabsTrigger>
</TabsList>
<SafeMenu />
</div>
<TabPanel id="account">Account settings</TabPanel>
<TabPanel id="security">Security settings</TabPanel>
</Tabs>
)
}Key Points
- Wrap a function, not the component itself:
createHideableComponent(function () { ... }) - Include dependencies in
useMemoif your Menu uses external state - This is only needed when Menu is a sibling to TabsList within the Tabs tree
- The same pattern applies to other collection-based components like
Select,ComboBox, etc.
API Reference
Tabs
The root tabs component that manages the selection state.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultSelectedKey | Key | - | The initial selected tab (uncontrolled) |
selectedKey | Key | - | The currently selected tab (controlled) |
onSelectionChange | (key: Key) => void | - | Handler called when tab selection changes |
orientation | 'horizontal' | 'vertical' | 'horizontal' | The orientation of the tabs |
isDisabled | boolean | false | Whether all tabs are disabled |
TabsList
Container for the tab triggers.
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | - | Accessible label for the tab list |
children | ReactNode | - | The tab triggers to display |
TabsTrigger
Individual tab trigger button.
| Prop | Type | Default | Description |
|---|---|---|---|
id | Key | - | Unique identifier for the tab |
isDisabled | boolean | false | Whether the tab is disabled |
children | ReactNode | - | The content to display in the tab |
TabPanel
The panel that displays the content for a selected tab.
| Prop | Type | Default | Description |
|---|---|---|---|
id | Key | - | Must match the id of the corresponding trigger |
children | ReactNode | - | The content to display when tab is selected |
Accessibility
- Uses the ARIA tabs pattern
- Keyboard navigation with arrow keys
- Tab key moves focus in and out of the tab list
- Supports automatic and manual activation modes
- Proper ARIA roles and attributes