Tabs are a navigation component used to switch between related content within the same view.
You’ll commonly see them in:
Settings pages
Dashboards
Profile sections
Analytics panels
Without tabs, users would need to navigate between multiple pages or deal with long scrolling sections — both reduce clarity and usability.
In this part, we’ll build a clean, accessible Tabs component using Radix UI, understand how it works under the hood, and explore real-world usage patterns in React applications.
When Should You Use Tabs?
Use Tabs when:
- You need to group related content in one layout
- Switching views should not trigger full page navigation
- You want clean content separation
- The number of sections is limited (3–6 works best)
Avoid Tabs when:
- You have too many sections (consider sidebar navigation instead)
- Content requires deep linking without controlled state
- Mobile layout becomes too cramped
Tabs are for structured grouping — not dumping unlimited sections.
Installation
Tabs in shadcn/ui are built on top of Radix UI.
Install dependency:
npm install @radix-ui/react-tabs
npm install class-variance-authorityFile Structure
src/
└─ components/
└─ ui/
├─ tabs.tsx
└─ tab.variants.tsTabs Component
components/ui/tabs.tsx
The Tabs component is composed of small primitives:
Tabs → Root container
TabsList → Wrapper for triggers
TabsTrigger → Clickable tab button
TabsContent → Content panel
Get Rivai Nasution’s stories in your inbox
Join Medium for free to get updates from this writer.
This composable structure keeps it flexible and fully controlled by Radix.
tabs.tsx
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
import { tabsListVariants, tabsTriggerVariants } from "./tab.variants";
import type { VariantProps } from "class-variance-authority";
function Tabs({
className,
orientation = "horizontal",
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
orientation={orientation}
className={cn(
"flex gap-4",
orientation === "vertical" && "flex-row",
orientation === "horizontal" && "flex-col",
className,
)}
{...props}
/>
);
}
function TabsList({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(tabsListVariants({ variant, size }), className)}
{...props}
/>
);
}
function TabsTrigger({
className,
variant,
disabled,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger> &
VariantProps<typeof tabsTriggerVariants>) {
return (
<TabsPrimitive.Trigger
disabled={disabled}
data-disabled={disabled ? "" : undefined}
className={cn(
tabsTriggerVariants({ variant }),
"inline-flex items-center gap-2 whitespace-nowrap px-3 py-1.5 text-sm font-medium transition-all",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"data-[state=active]:text-foreground data-[state=active]:font-semibold",
"disabled:pointer-events-none disabled:opacity-40 disabled:cursor-not-allowed",
"rtl:flex-row-reverse rtl:text-right",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
className={cn(
"flex-1 outline-none",
"focus-visible:ring-2 focus-visible:ring-ring",
className,
)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };tab.variants.ts
import { cva } from "class-variance-authority";
const tabsListVariants = cva("inline-flex items-center justify-start", {
variants: {
variant: {
default: "bg-muted p-1 rounded-lg",
line: "border-b border-border",
vertical: "flex-col border-r border-border",
},
size: {
sm: "text-sm",
md: "text-base",
lg: "text-lg",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
});
const tabsTriggerVariants = cva(
"relative inline-flex items-center justify-center gap-2 font-medium transition-all whitespace-nowrap",
{
variants: {
variant: {
default:
"rounded-md px-3 py-1.5 text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground",
line: "px-4 py-2 text-muted-foreground hover:text-foreground border-b-2 border-transparent data-[state=active]:border-foreground data-[state=active]:text-foreground rounded-none",
vertical:
"justify-start px-3 py-2 rounded-md text-muted-foreground hover:text-foreground data-[state=active]:bg-muted data-[state=active]:text-foreground w-full",
},
disabled: {
true: "opacity-50 pointer-events-none",
},
},
defaultVariants: {
variant: "default",
},
},
);
export { tabsListVariants, tabsTriggerVariants };Horizontal Line
<Tabs defaultValue="overview" orientation="horizontal">
<TabsList variant="line">
<TabsTrigger value="overview" variant="line">
Overview
</TabsTrigger>
<TabsTrigger value="analytics" variant="line">
Analytics
</TabsTrigger>
</TabsList>
<TabsContent value="overview">Overview content</TabsContent>
<TabsContent value="analytics">Analytics content</TabsContent>
</Tabs>Vertical Variant
<Tabs defaultValue="profile" orientation="vertical">
<TabsList variant="vertical">
<TabsTrigger value="profile" variant="vertical">
Profile
</TabsTrigger>
<TabsTrigger value="billing" variant="vertical">
Billing
</TabsTrigger>
</TabsList>
<TabsContent value="profile">Profile content</TabsContent>
<TabsContent value="billing">Billing content</TabsContent>
</Tabs>With Icon
<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">
<User size={16} />
Account
</TabsTrigger>
<TabsTrigger value="settings">
<Settings size={16} />
Settings
</TabsTrigger>
</TabsList>
<TabsContent value="account">Account content</TabsContent>
<TabsContent value="settings">Settings content</TabsContent>
</Tabs>Disabled
<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">
<User size={16} />
Account
</TabsTrigger>
<TabsTrigger value="settings" disabled>
<Settings size={16} />
Settings
</TabsTrigger>
</TabsList>
<TabsContent value="account">Account content</TabsContent>
<TabsContent value="settings">Settings content</TabsContent>
</Tabs>RTL Suport
<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">
<User size={16} />
الحساب
</TabsTrigger>
<TabsTrigger value="settings">
<Settings size={16} />
الإعدادات
</TabsTrigger>
</TabsList>
<TabsContent value="account">محتوى الحساب</TabsContent>
<TabsContent value="settings">محتوى الإعدادات</TabsContent>
</Tabs>Accessibility Notes
Tabs include:
- Proper ARIA roles
- Keyboard navigation support
- Focus management
- Data-state indicators
Best practices:
- Keep labels clear and short
- Avoid too many tabs
- Ensure mobile readability
- Use controlled mode if syncing with URL
Tabs vs Navigation Menu
Tabs:
Better for switching related content inside the same page
Cleaner for settings and panels
State-based navigation
Navigation Menu:
Better for multi-page navigation
More scalable
SEO-friendly
Choose Tabs when content belongs together.
Best Practices
Keep tabs limited (3–6 max)
Use clear naming
Avoid overloading content
Consider vertical layout for admin dashboards
Sync tab state with URL if needed
Summary
Tabs organize related content without page reload
Built on Radix UI for accessibility
Composable and flexible
Supports horizontal and vertical layouts
Perfect for dashboards, settings, and panels
A clean UI is not about adding components —
it’s about structuring information properly.











