Tabs
A set of layered sections of content, displayed one at a time.
Tabs organize content into multiple sections, allowing users to navigate between them. Built on React Aria's Tabs component with full keyboard navigation and accessibility support.
Basic Usage
A simple tabs component with three panels.
Configure your account details, profile information, and personal preferences here.
import { Tabs, TabList, Tab, TabPanel } from "@repo/ui/components/tabs";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/ui/components/card";
/* BASIC USAGE EXAMPLE */
export const Example1 = () => {
return (
<div className="w-full max-w-md">
<Tabs>
<TabList aria-label="Settings">
<Tab id="account">Account</Tab>
<Tab id="password">Password</Tab>
<Tab id="notifications">Notifications</Tab>
</TabList>
<TabPanel id="account">
<Card>
<CardHeader>
<CardTitle>Account Settings</CardTitle>
<CardDescription>
Manage your account settings and preferences.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">
Configure your account details, profile
information, and personal preferences here.
</p>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="password">
<Card>
<CardHeader>
<CardTitle>Password & Security</CardTitle>
<CardDescription>
Change your password and security settings.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">
Update your password, enable two-factor
authentication, and manage security preferences.
</p>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="notifications">
<Card>
<CardHeader>
<CardTitle>Notification Preferences</CardTitle>
<CardDescription>
Configure how you receive notifications.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">
Choose which notifications you want to receive
via email, push, or in-app alerts.
</p>
</CardContent>
</Card>
</TabPanel>
</Tabs>
</div>
);
};
Controlled Tabs
Use selectedKey and onSelectionChange props to control the active tab programmatically.
1,234
Total Users
567
Active Today
Selected: overview
"use client";
import { useState } from "react";
import type { Key } from "react-aria-components";
import { Tabs, TabList, Tab, TabPanel } from "@repo/ui/components/tabs";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/ui/components/card";
/* CONTROLLED TABS EXAMPLE */
export const Example2 = () => {
const [selectedTab, setSelectedTab] = useState<Key>("overview");
return (
<div className="w-full max-w-md space-y-4">
<Tabs selectedKey={selectedTab} onSelectionChange={setSelectedTab}>
<TabList aria-label="Dashboard">
<Tab id="overview">Overview</Tab>
<Tab id="analytics">Analytics</Tab>
<Tab id="reports">Reports</Tab>
</TabList>
<TabPanel id="overview">
<Card>
<CardHeader>
<CardTitle>Dashboard Overview</CardTitle>
<CardDescription>
Key metrics and summaries at a glance.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="bg-muted rounded-lg p-4">
<p className="text-2xl font-bold">1,234</p>
<p className="text-muted-foreground text-xs">
Total Users
</p>
</div>
<div className="bg-muted rounded-lg p-4">
<p className="text-2xl font-bold">567</p>
<p className="text-muted-foreground text-xs">
Active Today
</p>
</div>
</div>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="analytics">
<Card>
<CardHeader>
<CardTitle>Analytics</CardTitle>
<CardDescription>
Detailed performance data and insights.
</CardDescription>
</CardHeader>
<CardContent>
<div className="border-muted flex h-32 items-center justify-center rounded-lg border-2 border-dashed">
<p className="text-muted-foreground text-sm">
Analytics Chart
</p>
</div>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="reports">
<Card>
<CardHeader>
<CardTitle>Reports</CardTitle>
<CardDescription>
Generate and view detailed reports.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="bg-muted flex items-center justify-between rounded-lg p-3">
<span className="text-sm">
Monthly Report
</span>
<span className="text-muted-foreground text-xs">
Jan 2024
</span>
</div>
<div className="bg-muted flex items-center justify-between rounded-lg p-3">
<span className="text-sm">
Weekly Report
</span>
<span className="text-muted-foreground text-xs">
Week 4
</span>
</div>
</div>
</CardContent>
</Card>
</TabPanel>
</Tabs>
<p className="text-muted-foreground text-center text-sm">
Selected:{" "}
<span className="text-foreground font-medium">
{selectedTab}
</span>
</p>
</div>
);
};
Disabled Tabs
Use isDisabled on individual tabs to prevent interaction.
You have full access to this feature. Explore all the available options and settings.
import { Tabs, TabList, Tab, TabPanel } from "@repo/ui/components/tabs";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/ui/components/card";
import { Lock } from "lucide-react";
/* DISABLED TABS EXAMPLE */
export const Example3 = () => {
return (
<div className="w-full max-w-md">
<Tabs>
<TabList aria-label="Features">
<Tab id="active">Active</Tab>
<Tab id="disabled" isDisabled>
Disabled
</Tab>
<Tab id="premium" isDisabled>
<Lock className="size-3" />
Premium
</Tab>
<Tab id="settings">Settings</Tab>
</TabList>
<TabPanel id="active">
<Card>
<CardHeader>
<CardTitle>Active Feature</CardTitle>
<CardDescription>
This feature is available and fully accessible.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">
You have full access to this feature. Explore
all the available options and settings.
</p>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="disabled">
<Card>
<CardContent>
<p className="text-muted-foreground text-sm">
This content is not accessible.
</p>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="premium">
<Card>
<CardContent>
<p className="text-muted-foreground text-sm">
Premium content requires an upgrade.
</p>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="settings">
<Card>
<CardHeader>
<CardTitle>Settings</CardTitle>
<CardDescription>
Adjust your preferences and configuration.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">
Customize your experience with various settings
and options available here.
</p>
</CardContent>
</Card>
</TabPanel>
</Tabs>
</div>
);
};
Default Selected
Use defaultSelectedKey to set the initially selected tab for uncontrolled usage.
import { Tabs, TabList, Tab, TabPanel } from "@repo/ui/components/tabs";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/ui/components/card";
/* DEFAULT SELECTED TAB EXAMPLE */
export const Example4 = () => {
return (
<div className="w-full max-w-md">
<Tabs defaultSelectedKey="billing">
<TabList aria-label="Account settings">
<Tab id="profile">Profile</Tab>
<Tab id="billing">Billing</Tab>
<Tab id="team">Team</Tab>
</TabList>
<TabPanel id="profile">
<Card>
<CardHeader>
<CardTitle>Profile</CardTitle>
<CardDescription>
Manage your public profile information.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<div className="bg-muted flex size-12 items-center justify-center rounded-full text-lg font-medium">
JD
</div>
<div>
<p className="font-medium">John Doe</p>
<p className="text-muted-foreground text-sm">
john@example.com
</p>
</div>
</div>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="billing">
<Card>
<CardHeader>
<CardTitle>Billing</CardTitle>
<CardDescription>
View and manage your billing details.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm">
Current Plan
</span>
<span className="bg-primary/10 text-primary rounded-full px-2 py-1 text-xs font-medium">
Pro
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">
Next billing date
</span>
<span className="text-muted-foreground text-sm">
Feb 1, 2024
</span>
</div>
</div>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="team">
<Card>
<CardHeader>
<CardTitle>Team</CardTitle>
<CardDescription>
Invite and manage team members.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="bg-muted flex items-center gap-3 rounded-lg p-2">
<div className="bg-background flex size-8 items-center justify-center rounded-full text-sm font-medium">
JD
</div>
<div className="flex-1">
<p className="text-sm font-medium">
John Doe
</p>
<p className="text-muted-foreground text-xs">
Owner
</p>
</div>
</div>
<div className="bg-muted flex items-center gap-3 rounded-lg p-2">
<div className="bg-background flex size-8 items-center justify-center rounded-full text-sm font-medium">
JS
</div>
<div className="flex-1">
<p className="text-sm font-medium">
Jane Smith
</p>
<p className="text-muted-foreground text-xs">
Member
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</TabPanel>
</Tabs>
</div>
);
};
With Icons
Tabs can include icons alongside text labels.
Upload a new avatar
JPG, PNG or GIF. Max 2MB.
import { Tabs, TabList, Tab, TabPanel } from "@repo/ui/components/tabs";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/ui/components/card";
import { User, Bell, Shield } from "lucide-react";
/* TABS WITH ICONS EXAMPLE */
export const Example5 = () => {
return (
<div className="w-full max-w-md">
<Tabs>
<TabList aria-label="Settings">
<Tab id="profile">
<User className="size-4" />
Profile
</Tab>
<Tab id="notifications">
<Bell className="size-4" />
Alerts
</Tab>
<Tab id="security">
<Shield className="size-4" />
Security
</Tab>
</TabList>
<TabPanel id="profile">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="size-5" />
Profile Settings
</CardTitle>
<CardDescription>
Update your profile information and avatar.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<div className="flex size-16 items-center justify-center rounded-full bg-gradient-to-br from-blue-500 to-purple-500 text-xl font-bold text-white">
U
</div>
<div className="space-y-1">
<p className="font-medium">
Upload a new avatar
</p>
<p className="text-muted-foreground text-sm">
JPG, PNG or GIF. Max 2MB.
</p>
</div>
</div>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="notifications">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="size-5" />
Notification Settings
</CardTitle>
<CardDescription>
Configure your notification preferences.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm">
Email notifications
</span>
<span className="rounded-full bg-green-500/10 px-2 py-0.5 text-xs font-medium text-green-600">
On
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">
Push notifications
</span>
<span className="rounded-full bg-green-500/10 px-2 py-0.5 text-xs font-medium text-green-600">
On
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">SMS alerts</span>
<span className="bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-medium">
Off
</span>
</div>
</div>
</CardContent>
</Card>
</TabPanel>
<TabPanel id="security">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="size-5" />
Security Settings
</CardTitle>
<CardDescription>
Manage security and privacy settings.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm">
Two-factor authentication
</span>
<span className="rounded-full bg-green-500/10 px-2 py-0.5 text-xs font-medium text-green-600">
Enabled
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">
Last password change
</span>
<span className="text-muted-foreground text-sm">
30 days ago
</span>
</div>
</div>
</CardContent>
</Card>
</TabPanel>
</Tabs>
</div>
);
};
Dynamic Tabs
Use the items prop on TabList for dynamic collections.
"use client";
import { useState } from "react";
import { Tabs, TabList, Tab, TabPanel } from "@repo/ui/components/tabs";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/ui/components/card";
/* DYNAMIC TABS EXAMPLE */
export const Example6 = () => {
const [tabs] = useState([
{
id: "1",
title: "Project Alpha",
description: "Main development project",
status: "Active",
progress: 75,
},
{
id: "2",
title: "Project Beta",
description: "Research and development",
status: "In Progress",
progress: 45,
},
{
id: "3",
title: "Project Gamma",
description: "Client deliverable",
status: "Review",
progress: 90,
},
]);
return (
<div className="w-full max-w-md">
<Tabs>
<TabList aria-label="Projects" items={tabs}>
{(item) => <Tab id={item.id}>{item.title}</Tab>}
</TabList>
{tabs.map((item) => (
<TabPanel key={item.id} id={item.id}>
<Card>
<CardHeader>
<CardTitle>{item.title}</CardTitle>
<CardDescription>
{item.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm">Status</span>
<span className="bg-primary/10 text-primary rounded-full px-2 py-0.5 text-xs font-medium">
{item.status}
</span>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span>Progress</span>
<span className="font-medium">
{item.progress}%
</span>
</div>
<div className="bg-muted h-2 rounded-full">
<div
className="bg-primary h-full rounded-full transition-all"
style={{
width: `${item.progress}%`,
}}
/>
</div>
</div>
</CardContent>
</Card>
</TabPanel>
))}
</Tabs>
</div>
);
};
With Cards
Combine Tabs with Card components for a polished form interface.
import { Tabs, TabList, Tab, TabPanel } from "@repo/ui/components/tabs";
import { Button } from "@repo/ui/components/button";
import { Input } from "@repo/ui/components/input";
import { Label } from "@repo/ui/components/label";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@repo/ui/components/card";
/* TABS WITH CARDS EXAMPLE */
export const Example7 = () => {
return (
<div className="w-full max-w-md">
<Tabs defaultSelectedKey="account">
<TabList aria-label="Account settings">
<Tab id="account">Account</Tab>
<Tab id="password">Password</Tab>
</TabList>
<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-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="Pedro Duarte" />
</div>
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Input id="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-4">
<div className="grid gap-2">
<Label htmlFor="current">
Current password
</Label>
<Input id="current" type="password" />
</div>
<div className="grid gap-2">
<Label htmlFor="new">New password</Label>
<Input id="new" type="password" />
</div>
</CardContent>
<CardFooter>
<Button>Save password</Button>
</CardFooter>
</Card>
</TabPanel>
</Tabs>
</div>
);
};
Component Code
"use client";
import {
Tabs as AriaTabs,
TabList as AriaTabList,
Tab as AriaTab,
TabPanel as AriaTabPanel,
composeRenderProps,
type TabsProps as AriaTabsProps,
type TabListProps as AriaTabListProps,
type TabProps as AriaTabProps,
type TabPanelProps as AriaTabPanelProps,
} from "react-aria-components";
import { cn, tv } from "tailwind-variants";
import { focusRing } from "@repo/ui/lib/utils";
/* -----------------------------------------------------------------------------
* Tabs (Root)
* ---------------------------------------------------------------------------*/
interface TabsProps extends AriaTabsProps {}
const tabsStyles = tv({
variants: {
orientation: {
horizontal: "flex-col",
vertical: "flex-row",
},
},
defaultVariants: {
orientation: "horizontal",
},
});
export const Tabs = ({ className, ...props }: TabsProps) => {
return (
<AriaTabs
data-slot="tabs"
className={composeRenderProps(className, (className, renderProps) =>
tabsStyles({ ...renderProps, className }),
)}
{...props}
/>
);
};
/* -----------------------------------------------------------------------------
* TabList
* ---------------------------------------------------------------------------*/
interface TabListProps<T extends object> extends AriaTabListProps<T> {}
const tabListStyles = tv({
base: "bg-muted text-muted-foreground inline-flex w-fit items-center justify-center rounded-lg p-1",
variants: {
orientation: {
horizontal: "flex-row",
vertical: "flex-col items-start",
},
},
defaultVariants: {
orientation: "horizontal",
},
});
export const TabList = <T extends object>({
className,
...props
}: TabListProps<T>) => {
return (
<AriaTabList
data-slot="tab-list"
className={composeRenderProps(className, (className, renderProps) =>
tabListStyles({ ...renderProps, className }),
)}
{...props}
/>
);
};
/* -----------------------------------------------------------------------------
* Tab (Trigger)
* ---------------------------------------------------------------------------*/
interface TabProps extends AriaTabProps {}
const tabStyles = tv({
extend: focusRing,
base: [
"inline-flex flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-3 py-1.5 text-sm font-medium transition-[color,box-shadow]",
"text-foreground dark:text-muted-foreground",
"disabled:pointer-events-none disabled:opacity-50",
"[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
],
variants: {
isSelected: {
true: [
"bg-background text-foreground shadow-sm",
"dark:bg-input/30 dark:text-foreground dark:border-input",
],
false: "hover:text-foreground hover:bg-background/50",
},
isDisabled: {
true: "pointer-events-none opacity-50",
},
},
defaultVariants: {
isSelected: false,
},
});
export const Tab = ({ className, ...props }: TabProps) => {
return (
<AriaTab
data-slot="tab-trigger"
className={composeRenderProps(
cn("cursor-default", className),
(className, renderProps) =>
tabStyles({ ...renderProps, className }),
)}
{...props}
/>
);
};
/* -----------------------------------------------------------------------------
* TabPanel (Content)
* ---------------------------------------------------------------------------*/
interface TabPanelProps extends AriaTabPanelProps {}
const tabPanelStyles = tv({
extend: focusRing,
base: "text-foreground flex-1 pt-2 text-sm outline-none",
});
export const TabPanel = ({ className, ...props }: TabPanelProps) => {
return (
<AriaTabPanel
data-slot="tab-panel"
className={composeRenderProps(className, (className, renderProps) =>
tabPanelStyles({ ...renderProps, className }),
)}
{...props}
/>
);
};