# Shadcn Tabs

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

Source: https://designrevision.com/components/tabs

The **Shadcn Tabs** switch between panels in place — the canonical <a href="https://ui.shadcn.com/docs/components/tabs" rel="nofollow">shadcn/ui</a> tabs built on <a href="https://www.radix-ui.com/primitives/docs/components/tabs" rel="nofollow">Radix</a>. Official ships one demo in the default segmented style; the examples below cover what people actually search for — **underline tabs, pill tabs, vertical tabs**, plus icons, badges, full-width, controlled, and scrollable.

## Built on Radix

Tabs is a thin wrapper over <a href="https://www.radix-ui.com/primitives/docs/components/tabs" rel="nofollow">Radix Tabs</a> with four parts — `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`. Radix owns the <a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabs/" rel="nofollow">WAI-ARIA tabs pattern</a>: roving focus, arrow-key navigation, and the `aria-controls` wiring. The one rule: **each `TabsTrigger` `value` must match a `TabsContent` `value`.**

```tsx
<Tabs defaultValue="account">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">…</TabsContent>
  <TabsContent value="password">…</TabsContent>
</Tabs>
```

## Restyling: underline & pills

The default look is a **segmented card** — a `bg-muted` list where the active trigger gets `bg-background` + a shadow. The two most-requested alternatives are **className overrides**, not new components:

- **Underline** — make the list transparent with a bottom border (`bg-transparent rounded-none border-b p-0`) and give the trigger a `border-b-2 border-transparent data-[state=active]:border-primary`, clearing the card look with `data-[state=active]:bg-transparent data-[state=active]:shadow-none`.
- **Pills** — add `rounded-full` to the list and triggers and a solid active fill (`data-[state=active]:bg-primary data-[state=active]:text-primary-foreground`).

The Underline and Pills examples are copy-paste ready.

## Vertical, full-width & scrollable

- **Vertical** — set `orientation="vertical"` (for arrow keys), make `Tabs` `flex-row`, and the `TabsList` `flex-col` with `w-full justify-start` triggers (the Vertical example).
- **Full width** — give `TabsList` `grid w-full grid-cols-N` so every trigger is equal width (the Full width example).
- **Scrollable** — wrap a `w-max` `TabsList` in an `overflow-x-auto` div when there are too many tabs to fit (the Scrollable example).

## Icons, badges, disabled & controlled

Drop an icon and a count [Badge](/components/badge) straight into a trigger (the Icons & badges example). Disable a trigger with the `disabled` prop. Use `defaultValue` for an uncontrolled start, or `value` + `onValueChange` to **control** the active tab from state — useful for wizards or syncing to the URL (the Disabled & controlled example).

## Tabs vs Accordion

Use **Tabs** when the sections are peers shown one at a time in a fixed area (settings, preview/code). Use an [Accordion](/components/accordion) when sections stack and several may be open, or on narrow screens where horizontal triggers don't fit. Tabs switch in place; accordions expand downward.

## Accessibility & theming

Radix gives the tabs full keyboard support — arrow keys move between triggers, Home/End jump to the ends, and the panel is announced via `aria-controls`. The tabs read your design tokens — `--muted` (the list), `--background` (the active trigger), `--primary` (underline/pill accents) — so they follow your theme, including dark mode, with no overrides.

## Installation

### tabs

**Install:**

```bash
npx shadcn@latest add @designrevision/tabs
```

**Dependencies:** @radix-ui/react-tabs, utils

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| 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" \| "vertical" | "horizontal" | Tab orientation for arrow-key navigation (on Tabs). |

**Usage & accessibility:** 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).

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/tabs-demo-01
```

**Dependencies:** tabs, button, input, label

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/tabs-underline-01
```

**Dependencies:** tabs

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/tabs-pills-01
```

**Dependencies:** tabs

```tsx
// 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"`).

**Install:**

```bash
npx shadcn@latest add @designrevision/tabs-vertical-01
```

**Dependencies:** tabs

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/tabs-icons-01
```

**Dependencies:** lucide-react, tabs, badge

```tsx
// 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`.

**Install:**

```bash
npx shadcn@latest add @designrevision/tabs-full-width-01
```

**Dependencies:** tabs

```tsx
// 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`.

**Install:**

```bash
npx shadcn@latest add @designrevision/tabs-disabled-01
```

**Dependencies:** tabs

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/tabs-scrollable-01
```

**Dependencies:** tabs

```tsx
// 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>
  )
}
```
