Dependencies
tailwindcssshadcn/uilucide-react
shadcn/ui components
npx shadcn@latest add tabsnpx shadcn@latest add cardnpx shadcn@latest add badge

React tab components built with shadcn/ui and Tailwind CSS. Five patterns covering horizontal, vertical, icon, scrollable and pill-style tabs — all with controlled or uncontrolled state options.

Key Classes Reference

ComponentPurpose
TabsRoot — manages active tab state
TabsListContainer for trigger buttons
TabsTriggerIndividual tab button
TabsContentPanel rendered when tab is active
defaultValueUncontrolled initial active tab
value + onValueChangeControlled tab state

1. Basic Tabs

Standard horizontal tabs with shadcn/ui Tabs component.

tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"

export default function BasicTabs() {
  return (
    <div className="p-6">
      <Tabs defaultValue="overview">
        <TabsList className="grid w-full grid-cols-3">
          <TabsTrigger value="overview">Overview</TabsTrigger>
          <TabsTrigger value="analytics">Analytics</TabsTrigger>
          <TabsTrigger value="settings">Settings</TabsTrigger>
        </TabsList>
        <TabsContent value="overview">
          <Card>
            <CardHeader>
              <CardTitle>Overview</CardTitle>
              <CardDescription>Your dashboard summary for this month.</CardDescription>
            </CardHeader>
            <CardContent>
              <div className="grid grid-cols-3 gap-4">
                {[["Revenue", "$12,400"], ["Users", "1,293"], ["Orders", "847"]].map(([label, value]) => (
                  <div key={label} className="rounded-xl bg-gray-50 p-4">
                    <p className="text-xs text-muted-foreground">{label}</p>
                    <p className="text-2xl font-bold mt-1">{value}</p>
                  </div>
                ))}
              </div>
            </CardContent>
          </Card>
        </TabsContent>
        <TabsContent value="analytics">
          <Card>
            <CardHeader>
              <CardTitle>Analytics</CardTitle>
              <CardDescription>Detailed traffic and conversion data.</CardDescription>
            </CardHeader>
            <CardContent>
              <p className="text-sm text-muted-foreground">Analytics content goes here.</p>
            </CardContent>
          </Card>
        </TabsContent>
        <TabsContent value="settings">
          <Card>
            <CardHeader>
              <CardTitle>Settings</CardTitle>
              <CardDescription>Manage your account preferences.</CardDescription>
            </CardHeader>
            <CardContent>
              <p className="text-sm text-muted-foreground">Settings content goes here.</p>
            </CardContent>
          </Card>
        </TabsContent>
      </Tabs>
    </div>
  )
}

2. Icon Tabs

Tabs with icons and badge counts using lucide-react.

tsx
import { useState } from "react"
import { LayoutDashboard, Users, FileText, Bell } from "lucide-react"
import { Badge } from "@/components/ui/badge"

const tabs = [
  { id: "dashboard", label: "Dashboard", icon: LayoutDashboard, count: null },
  { id: "users",     label: "Users",     icon: Users,           count: 24   },
  { id: "docs",      label: "Docs",      icon: FileText,        count: null },
  { id: "alerts",    label: "Alerts",    icon: Bell,            count: 3    },
]

export default function IconTabs() {
  const [active, setActive] = useState("dashboard")

  return (
    <div className="p-6">
      <div className="flex border-b border-gray-200 gap-1">
        {tabs.map((tab) => (
          <button
            key={tab.id}
            onClick={() => setActive(tab.id)}
            className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
              active === tab.id
                ? "border-red-500 text-red-500"
                : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
            }`}
          >
            <tab.icon className="h-4 w-4" />
            {tab.label}
            {tab.count && (
              <Badge className="h-4 px-1.5 text-xs bg-red-500 text-white">{tab.count}</Badge>
            )}
          </button>
        ))}
      </div>
      <div className="p-4 text-sm text-muted-foreground">
        Active tab: <span className="font-semibold text-gray-900">{active}</span>
      </div>
    </div>
  )
}

3. Vertical Tabs

Sidebar-style vertical tab layout for settings pages.

tsx
import { useState } from "react"
import { User, Lock, Bell, CreditCard, HelpCircle } from "lucide-react"

const tabs = [
  { id: "profile",       label: "Profile",        icon: User        },
  { id: "security",      label: "Security",       icon: Lock        },
  { id: "notifications", label: "Notifications",  icon: Bell        },
  { id: "billing",       label: "Billing",        icon: CreditCard  },
  { id: "support",       label: "Support",        icon: HelpCircle  },
]

const content: Record<string, { title: string; desc: string }> = {
  profile:       { title: "Profile Settings",      desc: "Update your name, avatar and public profile information." },
  security:      { title: "Security",              desc: "Manage your password, two-factor authentication and sessions." },
  notifications: { title: "Notifications",         desc: "Choose what you want to be notified about." },
  billing:       { title: "Billing & Plans",       desc: "Manage your subscription and payment methods." },
  support:       { title: "Help & Support",        desc: "Get help from our team or browse documentation." },
}

export default function VerticalTabs() {
  const [active, setActive] = useState("profile")

  return (
    <div className="p-6">
      <div className="flex gap-6">
        <nav className="w-44 shrink-0 space-y-1">
          {tabs.map((tab) => (
            <button
              key={tab.id}
              onClick={() => setActive(tab.id)}
              className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors text-left ${
                active === tab.id
                  ? "bg-red-50 text-red-600"
                  : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
              }`}
            >
              <tab.icon className="h-4 w-4 shrink-0" />
              {tab.label}
            </button>
          ))}
        </nav>
        <div className="flex-1 rounded-xl border border-gray-200 p-6">
          <h3 className="font-bold text-lg mb-1">{content[active].title}</h3>
          <p className="text-sm text-muted-foreground">{content[active].desc}</p>
        </div>
      </div>
    </div>
  )
}

4. Scrollable Tabs

Horizontally scrollable tab bar for many tab items.

tsx
import { useState } from "react"

const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

const data: Record<string, { revenue: string; orders: number }> = {
  Jan: { revenue: "$8,200",  orders: 312 }, Feb: { revenue: "$9,100",  orders: 341 },
  Mar: { revenue: "$11,400", orders: 420 }, Apr: { revenue: "$10,200", orders: 389 },
  May: { revenue: "$12,800", orders: 467 }, Jun: { revenue: "$13,400", orders: 512 },
  Jul: { revenue: "$14,200", orders: 543 }, Aug: { revenue: "$13,800", orders: 521 },
  Sep: { revenue: "$12,100", orders: 462 }, Oct: { revenue: "$11,600", orders: 441 },
  Nov: { revenue: "$15,200", orders: 584 }, Dec: { revenue: "$18,400", orders: 702 },
}

export default function ScrollableTabs() {
  const [active, setActive] = useState("Jun")

  return (
    <div className="p-6">
      <div className="overflow-x-auto">
        <div className="flex border-b border-gray-200 min-w-max">
          {months.map((m) => (
            <button
              key={m}
              onClick={() => setActive(m)}
              className={`px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap transition-colors ${
                active === m
                  ? "border-red-500 text-red-500"
                  : "border-transparent text-gray-500 hover:text-gray-700"
              }`}
            >
              {m}
            </button>
          ))}
        </div>
      </div>
      <div className="mt-6 grid grid-cols-2 gap-4">
        <div className="rounded-xl bg-red-50 p-5">
          <p className="text-xs text-red-400 font-medium uppercase tracking-wide">Revenue</p>
          <p className="text-3xl font-bold text-red-600 mt-1">{data[active].revenue}</p>
        </div>
        <div className="rounded-xl bg-gray-50 p-5">
          <p className="text-xs text-gray-400 font-medium uppercase tracking-wide">Orders</p>
          <p className="text-3xl font-bold text-gray-900 mt-1">{data[active].orders}</p>
        </div>
      </div>
    </div>
  )
}

5. Pill Tabs

Pill-shaped tab buttons with background highlight instead of underline.

tsx
import { useState } from "react"

const tabs = [
  { id: "all",       label: "All Templates",  count: 28 },
  { id: "angular",   label: "Angular",         count: 12 },
  { id: "react",     label: "React",           count: 8  },
  { id: "bootstrap", label: "Bootstrap",       count: 8  },
]

const templates = [
  { name: "Marvel Dashboard", framework: "angular" },
  { name: "PORTO",            framework: "bootstrap" },
  { name: "Kiosk",            framework: "react" },
  { name: "Proctu",           framework: "react" },
  { name: "ORYO",             framework: "angular" },
  { name: "Consult",          framework: "bootstrap" },
]

export default function PillTabs() {
  const [active, setActive] = useState("all")

  const filtered = active === "all"
    ? templates
    : templates.filter((t) => t.framework === active)

  return (
    <div className="p-6">
      <div className="flex flex-wrap gap-2 mb-6 bg-gray-100 p-1 rounded-xl w-fit">
        {tabs.map((tab) => (
          <button
            key={tab.id}
            onClick={() => setActive(tab.id)}
            className={`flex items-center gap-1.5 px-4 py-1.5 rounded-lg text-sm font-medium transition-all ${
              active === tab.id
                ? "bg-white text-red-500 shadow-sm"
                : "text-gray-500 hover:text-gray-700"
            }`}
          >
            {tab.label}
            <span className={`text-xs px-1.5 py-0.5 rounded-full ${
              active === tab.id ? "bg-red-50 text-red-500" : "bg-gray-200 text-gray-500"
            }`}>
              {tab.id === "all" ? filtered.length : templates.filter(t => t.framework === tab.id).length}
            </span>
          </button>
        ))}
      </div>
      <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
        {filtered.map((t) => (
          <div key={t.name} className="rounded-xl border border-gray-200 p-4">
            <p className="font-semibold text-sm">{t.name}</p>
            <p className="text-xs text-muted-foreground capitalize mt-0.5">{t.framework}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

Frequently Asked Questions

Import Tabs, TabsList, TabsTrigger, and TabsContent from shadcn/ui. Set a defaultValue on the Tabs root matching one of your TabsTrigger value props. Each TabsContent renders only when its value matches the active tab.
Use the value and onValueChange props on the Tabs component instead of defaultValue. Store the active tab in useState and pass it as value. Call onValueChange with a setter to update it: <Tabs value={tab} onValueChange={setTab}>.

Need a Full React + Next.js Dashboard Template?

Get a complete React + Next.js dashboard with 50+ components — built by the same team behind BootstrapPlanet.

Browse Templates →

Use code FIRST30 for 30% off.

Related Components