Sitemap
Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

[Shadcn/ui React Series — Part 25] Tabs: Organizing Content Without Leaving the Page

5 min readFeb 12, 2026

--

Press enter or click to view image in full size

Shadcn/ui React Series Navigation Components:

Read documentation here

Demo in here

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-authority

File Structure

src/
└─ components/
└─ ui/
├─ tabs.tsx
└─ tab.variants.ts

Tabs 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

Press enter or click to view image in full size
<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

Press enter or click to view image in full size
<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

Press enter or click to view image in full size
<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

Press enter or click to view image in full size
<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

Press enter or click to view image in full size
<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.

--

--