The Tabs component organizes content into multiple panels, allowing users to switch between them using tab labels. Built on Base UI’s Tabs primitive with support for keyboard navigation and focus management.
Installation
npx shadcn@latest add @eo-n/tabs
Copy the component code
Copy and paste the Tabs component code into your project at components/ui/tabs.tsx."use client";
import * as React from "react";
import { Tabs as TabsPrimitive } from "@base-ui/react";
import { cn } from "@/lib/utils";
interface TabsProps extends React.ComponentProps<typeof TabsPrimitive.Root> {
variant?: "underline" | "default";
}
function Tabs({ className, variant = "default", ...props }: TabsProps) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-variant={variant}
className={cn(
"group flex flex-col gap-1.5 data-[orientation=vertical]:flex-row",
className
)}
{...props}
/>
);
}
function TabsList({
className,
children,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"text-muted-foreground relative z-0 flex items-center data-[orientation=horizontal]:h-9 data-[orientation=horizontal]:justify-center data-[orientation=vertical]:h-fit data-[orientation=vertical]:flex-col data-[orientation=vertical]:justify-start",
"group-data-[variant=default]:bg-muted group-data-[variant=default]:rounded-lg group-data-[variant=default]:p-[3px] group-data-[variant=underline]:bg-transparent group-data-[variant=underline]:data-[orientation=horizontal]:border-b group-data-[variant=underline]:data-[orientation=horizontal]:py-5 group-data-[variant=underline]:data-[orientation=vertical]:border-r group-data-[variant=underline]:data-[orientation=vertical]:px-1.5",
className
)}
{...props}
>
{children}
<TabsIndicator />
</TabsPrimitive.List>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Tab>) {
return (
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"ring-offset-background data-[active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-muted-foreground hover:text-foreground flex w-full items-center justify-center gap-1.5 rounded-md border border-transparent text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-[3px] focus-visible:outline-1 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[orientation=horizontal]:px-2 data-[orientation=horizontal]:py-1 data-[orientation=vertical]:px-1.5 data-[orientation=vertical]:py-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=underline]:hover:bg-muted group-data-[variant=underline]:dark:hover:border-input group-data-[variant=underline]:hover:border-border group-data-[variant=underline]:hover:border",
className
)}
{...props}
/>
);
}
function TabsIndicator({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Indicator>) {
return (
<TabsPrimitive.Indicator
data-slot="tabs-indicator"
renderBeforeHydration
className={cn(
"absolute z-[-1] h-[calc(var(--active-tab-height))] w-[var(--active-tab-width)] flex-1 rounded-md transition-all duration-300 ease-out focus-visible:ring-[3px] focus-visible:outline-1 data-[orientation=horizontal]:top-1/2 data-[orientation=horizontal]:left-0 data-[orientation=horizontal]:translate-x-[var(--active-tab-left)] data-[orientation=horizontal]:-translate-y-1/2 data-[orientation=vertical]:top-0 data-[orientation=vertical]:left-1/2 data-[orientation=vertical]:-translate-x-1/2 data-[orientation=vertical]:translate-y-[var(--active-tab-top)]",
"group-data-[variant=default]:bg-background group-data-[variant=default]:dark:bg-input/30 group-data-[variant=default]:dark:border-input group-data-[variant=underline]:after:bg-foreground group-data-[variant=default]:border group-data-[variant=default]:shadow-sm group-data-[variant=underline]:bg-transparent group-data-[variant=underline]:after:absolute group-data-[variant=underline]:after:content-[''] group-data-[variant=underline]:data-[orientation=horizontal]:after:-bottom-1.5 group-data-[variant=underline]:data-[orientation=horizontal]:after:h-[2px] group-data-[variant=underline]:data-[orientation=horizontal]:after:w-full group-data-[variant=underline]:data-[orientation=vertical]:after:-right-[7px] group-data-[variant=underline]:data-[orientation=vertical]:after:h-full group-data-[variant=underline]:data-[orientation=vertical]:after:w-[2px]",
className
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Panel>) {
return (
<TabsPrimitive.Panel
data-slot="tabs-content"
className={cn("outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsIndicator, TabsContent };
Update imports
Update the import paths to match your project setup.
Usage
Import the Tabs components and compose them together:
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">Account content</TabsContent>
<TabsContent value="password">Password content</TabsContent>
</Tabs>
Component API
Tabs
The root container component that manages tab state.
The value of the tab that should be active by default.
The controlled value of the active tab.
Callback fired when the active tab changes.
variant
'default' | 'underline'
default:"default"
The visual style variant of the tabs.
orientation
'horizontal' | 'vertical'
default:"horizontal"
The orientation of the tabs.
Additional CSS classes to apply.
TabsList
Container for the tab triggers.
Additional CSS classes to apply to the tabs list.
TabsTrigger
Individual tab button that activates a panel.
The unique value that identifies this tab.
Whether the tab is disabled.
Additional CSS classes to apply to the trigger.
TabsContent
Content panel associated with a tab.
The value that identifies which tab activates this content.
Additional CSS classes to apply to the content.
Examples
Default Tabs
<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 here</p>
</TabsContent>
<TabsContent value="analytics">
<p>Analytics content here</p>
</TabsContent>
<TabsContent value="reports">
<p>Reports content here</p>
</TabsContent>
</Tabs>
Underline Variant
<Tabs defaultValue="home" variant="underline">
<TabsList>
<TabsTrigger value="home">Home</TabsTrigger>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="home">Home content</TabsContent>
<TabsContent value="profile">Profile content</TabsContent>
<TabsContent value="settings">Settings content</TabsContent>
</Tabs>
Vertical Orientation
<Tabs defaultValue="general" orientation="vertical">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
<TabsTrigger value="notifications">Notifications</TabsTrigger>
</TabsList>
<TabsContent value="general">General settings</TabsContent>
<TabsContent value="security">Security settings</TabsContent>
<TabsContent value="notifications">Notification settings</TabsContent>
</Tabs>
Controlled Tabs
function ControlledTabs() {
const [activeTab, setActiveTab] = React.useState("tab1");
return (
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Content 1</TabsContent>
<TabsContent value="tab2">Content 2</TabsContent>
</Tabs>
);
}
Accessibility
The Tabs component is built on Base UI’s accessible Tabs primitive, which:
- Implements the ARIA tabs design pattern
- Supports full keyboard navigation (Arrow keys, Home, End)
- Manages focus correctly when switching tabs
- Properly associates tab triggers with their content panels
- Supports disabled states
For more details, see the Base UI Tabs documentation.