Free shadcn/ui tabs component for React — Radix-powered, with the default segmented look plus underline and pill restyles, vertical orientation, icons and badges, full-width, disabled, controlled, and scrollable. The examples official shadcn never shipped.

Installation

1 Configure registry — once per project
{
  "registries": {
    "@designrevision": "https://registry.designrevision.com/r/{name}.json"
  }
}

Add to your existing components.json. Requires shadcn/ui v2.3+.

2 Install the component

Packages

@radix-ui/react-tabs
utils

Props

defaultValue = —
string

The tab selected by default (uncontrolled, on Tabs).

value = —
string

Controlled active tab (on Tabs). Pair with onValueChange.

onValueChange = —
(value: string) => void

Fires when the active tab changes (on Tabs).

orientation = "horizontal"
"horizontal" | "vertical"

Tab orientation for arrow-key navigation (on Tabs).

A Radix tabs primitive — four parts (Tabs, TabsList, TabsTrigger, TabsContent). The default look is a segmented card (bg-muted list, active trigger gets bg-background + shadow). Match each TabsTrigger value to a TabsContent value. Restyle to underline or pills, or go vertical, by overriding the TabsList / TabsTrigger classNames (see the examples).

Copied!
components/ui/tabs.tsx
"use client"

import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"

import { cn } from "@/lib/utils"

function Tabs({
  className,
  ...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
  return (
    <TabsPrimitive.Root
      data-slot="tabs"
      className={cn("flex flex-col gap-2", className)}
      {...props}
    />
  )
}

function TabsList({
  className,
  ...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
  return (
    <TabsPrimitive.List
      data-slot="tabs-list"
      className={cn(
        "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
        className
      )}
      {...props}
    />
  )
}

function TabsTrigger({
  className,
  ...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
  return (
    <TabsPrimitive.Trigger
      data-slot="tabs-trigger"
      className={cn(
        "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    />
  )
}

function TabsContent({
  className,
  ...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
  return (
    <TabsPrimitive.Content
      data-slot="tabs-content"
      className={cn("flex-1 outline-none", className)}
      {...props}
    />
  )
}

export { Tabs, TabsList, TabsTrigger, TabsContent }

Examples

Tabs

The canonical account/password tabs — the default segmented-card look with two full-width triggers.

Packages

tabs
button
input
label

Props

No props documented yet.

Copied!
components/ui/tabs-demo-01.tsx
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

export default function TabsDemo01() {
  return (
    <Tabs defaultValue="account" className="w-[360px]">
      <TabsList className="grid w-full grid-cols-2">
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
      </TabsList>
      <TabsContent value="account" className="mt-4 grid gap-4">
        <div className="grid gap-2">
          <Label htmlFor="name">Name</Label>
          <Input id="name" defaultValue="Ada Lovelace" />
        </div>
        <div className="grid gap-2">
          <Label htmlFor="username">Username</Label>
          <Input id="username" defaultValue="@ada" />
        </div>
        <Button className="w-fit">Save changes</Button>
      </TabsContent>
      <TabsContent value="password" className="mt-4 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>
        <Button className="w-fit">Change password</Button>
      </TabsContent>
    </Tabs>
  )
}

Underline style

A line restyle — a transparent list with a bottom border and a `border-b-2` primary underline on the active tab.

Packages

tabs

Props

No props documented yet.

Copied!
components/ui/tabs-underline-01.tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  { value: "overview", body: "Your account overview at a glance." },
  { value: "analytics", body: "Traffic and engagement analytics." },
  { value: "reports", body: "Download and schedule reports." },
  { value: "notifications", body: "Manage your notification preferences." },
]

export default function TabsUnderline01() {
  return (
    <Tabs defaultValue="overview" className="w-[440px]">
      <TabsList className="h-auto w-full justify-start gap-4 rounded-none border-b bg-transparent p-0">
        {tabs.map((tab) => (
          <TabsTrigger
            key={tab.value}
            value={tab.value}
            className="text-muted-foreground data-[state=active]:border-primary data-[state=active]:text-foreground h-9 flex-none rounded-none border-0 border-b-2 border-transparent bg-transparent px-1 capitalize shadow-none data-[state=active]:bg-transparent data-[state=active]:shadow-none"
          >
            {tab.value}
          </TabsTrigger>
        ))}
      </TabsList>
      {tabs.map((tab) => (
        <TabsContent
          key={tab.value}
          value={tab.value}
          className="text-muted-foreground mt-4 text-sm"
        >
          {tab.body}
        </TabsContent>
      ))}
    </Tabs>
  )
}

Pills

A `rounded-full` pill restyle with a solid primary active state.

Packages

tabs

Props

No props documented yet.

Copied!
components/ui/tabs-pills-01.tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  { value: "all", label: "All", body: "Showing all items." },
  { value: "active", label: "Active", body: "Showing active items." },
  { value: "archived", label: "Archived", body: "Showing archived items." },
]

export default function TabsPills01() {
  return (
    <Tabs defaultValue="all" className="w-[420px]">
      <TabsList className="rounded-full">
        {tabs.map((tab) => (
          <TabsTrigger
            key={tab.value}
            value={tab.value}
            className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground rounded-full data-[state=active]:shadow-none"
          >
            {tab.label}
          </TabsTrigger>
        ))}
      </TabsList>
      {tabs.map((tab) => (
        <TabsContent
          key={tab.value}
          value={tab.value}
          className="text-muted-foreground mt-4 text-sm"
        >
          {tab.body}
        </TabsContent>
      ))}
    </Tabs>
  )
}

Vertical

Vertical tabs — `flex-row` Tabs with a `flex-col` TabsList and left-aligned triggers (`orientation="vertical"`).

Packages

tabs

Props

No props documented yet.

Copied!
components/ui/tabs-vertical-01.tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  { value: "general", label: "General", body: "General workspace settings." },
  { value: "members", label: "Members", body: "Invite and manage members." },
  { value: "billing", label: "Billing", body: "Plans and invoices." },
  { value: "advanced", label: "Advanced", body: "Danger zone and exports." },
]

export default function TabsVertical01() {
  return (
    <Tabs
      defaultValue="general"
      orientation="vertical"
      className="w-[460px] flex-row gap-4"
    >
      <TabsList className="h-auto w-40 flex-col">
        {tabs.map((tab) => (
          <TabsTrigger key={tab.value} value={tab.value} className="w-full justify-start">
            {tab.label}
          </TabsTrigger>
        ))}
      </TabsList>
      <div className="flex-1">
        {tabs.map((tab) => (
          <TabsContent
            key={tab.value}
            value={tab.value}
            className="text-muted-foreground text-sm"
          >
            {tab.body}
          </TabsContent>
        ))}
      </div>
    </Tabs>
  )
}

Icons & badges

Triggers with a leading icon and a count [Badge](/components/badge) — the inbox/sent/alerts pattern.

Packages

lucide-react
tabs
badge

Props

No props documented yet.

Copied!
components/ui/tabs-icons-01.tsx
import { BellIcon, InboxIcon, SendIcon } from "lucide-react"

import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  { value: "inbox", label: "Inbox", icon: InboxIcon, count: 12, body: "You have 12 new messages." },
  { value: "sent", label: "Sent", icon: SendIcon, count: 0, body: "Your sent messages." },
  { value: "alerts", label: "Alerts", icon: BellIcon, count: 3, body: "You have 3 unread alerts." },
]

export default function TabsIcons01() {
  return (
    <Tabs defaultValue="inbox" className="w-[440px]">
      <TabsList className="w-full">
        {tabs.map((tab) => (
          <TabsTrigger key={tab.value} value={tab.value}>
            <tab.icon />
            {tab.label}
            {tab.count > 0 && (
              <Badge
                variant="secondary"
                className="h-5 min-w-5 px-1 tabular-nums"
              >
                {tab.count}
              </Badge>
            )}
          </TabsTrigger>
        ))}
      </TabsList>
      {tabs.map((tab) => (
        <TabsContent
          key={tab.value}
          value={tab.value}
          className="text-muted-foreground mt-4 text-sm"
        >
          {tab.body}
        </TabsContent>
      ))}
    </Tabs>
  )
}

Full width

Equal-width triggers that fill the container with `grid w-full grid-cols-N`.

Packages

tabs

Props

No props documented yet.

Copied!
components/ui/tabs-full-width-01.tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  { value: "preview", label: "Preview", body: "The rendered preview." },
  { value: "code", label: "Code", body: "The source code." },
  { value: "settings", label: "Settings", body: "Configuration options." },
]

export default function TabsFullWidth01() {
  return (
    <Tabs defaultValue="preview" className="w-[460px]">
      <TabsList className="grid w-full grid-cols-3">
        {tabs.map((tab) => (
          <TabsTrigger key={tab.value} value={tab.value}>
            {tab.label}
          </TabsTrigger>
        ))}
      </TabsList>
      {tabs.map((tab) => (
        <TabsContent
          key={tab.value}
          value={tab.value}
          className="text-muted-foreground mt-4 rounded-md border p-4 text-sm"
        >
          {tab.body}
        </TabsContent>
      ))}
    </Tabs>
  )
}

Disabled & controlled

A disabled tab plus **controlled** state via `value` / `onValueChange`.

Packages

tabs

Props

No props documented yet.

Copied!
components/ui/tabs-disabled-01.tsx
"use client"

import * as React from "react"

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

export default function TabsDisabled01() {
  const [tab, setTab] = React.useState("details")

  return (
    <div className="grid gap-3">
      <p className="text-muted-foreground text-sm">
        Active tab: <span className="text-foreground font-medium">{tab}</span>
      </p>
      <Tabs value={tab} onValueChange={setTab} className="w-[420px]">
        <TabsList className="w-full">
          <TabsTrigger value="details">Details</TabsTrigger>
          <TabsTrigger value="shipping">Shipping</TabsTrigger>
          <TabsTrigger value="payment" disabled>
            Payment
          </TabsTrigger>
        </TabsList>
        <TabsContent value="details" className="text-muted-foreground mt-4 text-sm">
          Your order details.
        </TabsContent>
        <TabsContent value="shipping" className="text-muted-foreground mt-4 text-sm">
          Your shipping address.
        </TabsContent>
        <TabsContent value="payment" className="text-muted-foreground mt-4 text-sm">
          Payment unlocks once shipping is set.
        </TabsContent>
      </Tabs>
    </div>
  )
}

Scrollable

Many tabs in a horizontally scrollable list — `overflow-x-auto` around a `w-max` TabsList.

Packages

tabs

Props

No props documented yet.

Copied!
components/ui/tabs-scrollable-01.tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const months = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
]

export default function TabsScrollable01() {
  return (
    <Tabs defaultValue="January" className="w-[440px]">
      <div className="overflow-x-auto">
        <TabsList className="w-max">
          {months.map((month) => (
            <TabsTrigger key={month} value={month}>
              {month}
            </TabsTrigger>
          ))}
        </TabsList>
      </div>
      {months.map((month) => (
        <TabsContent
          key={month}
          value={month}
          className="text-muted-foreground mt-4 text-sm"
        >
          Showing data for {month}.
        </TabsContent>
      ))}
    </Tabs>
  )
}

Frequently asked questions

Yes — it is the canonical shadcn/ui tabs, built on Radix Tabs with four parts (Tabs, TabsList, TabsTrigger, TabsContent). Official ships one account/password demo with the default segmented-card look; we add underline and pill restyles, vertical orientation, icons and badges, full-width, disabled, controlled, and scrollable examples.
Restyle with classNames — no custom component needed. On TabsList use bg-transparent rounded-none border-b p-0; on each TabsTrigger use rounded-none border-0 border-b-2 border-transparent data-[state=active]:border-primary, and clear the default card look with data-[state=active]:bg-transparent data-[state=active]:shadow-none. The Underline style example is copy-paste ready.
Add rounded-full to the TabsList and each TabsTrigger, and set the active state to a solid fill: data-[state=active]:bg-primary data-[state=active]:text-primary-foreground (with data-[state=active]:shadow-none). The Pills example shows it.
Set orientation="vertical" on Tabs (for arrow-key navigation), make the Tabs container flex-row, and give the TabsList h-auto flex-col with w-full justify-start triggers so the labels stack on the left and the panel sits to the right. The Vertical example is the full pattern.
Use defaultValue for an uncontrolled starting tab, or value + onValueChange to control it from state (handy for syncing to a URL or a wizard step). Each TabsTrigger value must match a TabsContent value. The Disabled & controlled example controls the active tab and disables one trigger.
Use Tabs when the sections are peers and only one should be visible at a time in a fixed area — settings panels, account/password, preview/code. Use an Accordion when sections stack vertically and the user may want several open, or on narrow screens where horizontal triggers do not fit. Tabs switch in place; accordions expand down.
Yes — Radix implements the WAI-ARIA tabs pattern: the active trigger is focusable, the arrow keys move between tabs (roving focus), Home/End jump to the first/last, and the selected panel is associated via aria-controls. Set orientation="vertical" so the up/down arrows drive vertical tabs.