Skip to content

Tabs

Switches between panels of related content.

Library
What’s this?
Acme is a manufacturing customer in San Francisco, owned by Jordan Lee.

Installation

pnpm dlx shadcn@latest add @wisty/tabs

The CLI installs the Radix or Base UI file to match your project.

If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.

When to use

Use it

  • To switch between a few related views of one record or object, like an account's Activity, Details and Related.
  • The sections are peers. People visit them in any order, don't need two at once, and each has a one or two word label.
  • For a boxed tab set inside a card or region. Use variant="scoped" on TabsList.
  • For a settings or setup panel with more sections than fit on one row, as tabs in a column beside the panel. Use orientation="vertical" on Tabs with variant="scoped" on TabsList.

Don't use it

  • People need several sections at once. Use Accordion.
  • Each tab should open its own page or change the URL. Use your router's links in a nav, so the back button and open-in-new-tab work.
  • The sections are stages in a process, like an opportunity moving from Prospecting to Closed Won. Use Path.
  • You're switching between two or three ways to show the same data, like a list and a grid. Use Toggle Group.

Usage

Tabs
├── TabsList
│   └── TabsTrigger
└── TabsContent

Set defaultValue to the tab that opens first, and give every TabsTrigger a TabsContent with the same value.

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="overview">
  <TabsList aria-label="Opportunity">
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="history">History</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">{/* overview panel */}</TabsContent>
  <TabsContent value="history">{/* history panel */}</TabsContent>
</Tabs>

The two bases differ in when a tab is selected. In Radix projects the arrows select each tab they reach; set activationMode="manual" on Tabs to select with Enter or Space instead. In Base UI projects the arrows only move focus and Enter or Space selects; set activateOnFocus on TabsList to select as the arrows move. Without defaultValue or value, Radix projects select no tab and show no panel, and Base UI projects select the first tab that isn't disabled.

Examples

Scoped

Bordered tabs whose panel continues the border, set with `variant="scoped"` on `TabsList`, and Quotes `disabled` until a price book is chosen.

Stage Negotiation, amount $120,000, closes Nov 14, 2026.

Vertical

Tabs in a column beside their panel, set with `orientation="vertical"` on `Tabs` and `variant="scoped"` on `TabsList`.

Jordan Lee, Account Executive, in the Pacific time zone.

API reference

PartBuilt on
TabsRadix Tabs.Root (opens in a new tab)
TabsListRadix Tabs.List (opens in a new tab)
TabsTriggerRadix Tabs.Trigger (opens in a new tab)
TabsContentRadix Tabs.Content (opens in a new tab)

Tabs

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"The orientation the tabs are layed out.

TabsList

PropTypeDefault
variant"default" | "line" | "scoped""default"

SLDS mapping

Tabs is the slds-tabs_default wrapper, TabsList its slds-tabs_default__nav, TabsTrigger the slds-tabs_default__item and its slds-tabs_default__link in one button, and TabsContent is slds-tabs_default__content. With variant="scoped" each part takes the slds-tabs_scoped class instead, and aria-selected does the job of slds-is-active. With orientation="vertical" and variant="scoped", Tabs is slds-vertical-tabs, TabsList its slds-vertical-tabs__nav, TabsTrigger the slds-vertical-tabs__nav-item and its slds-vertical-tabs__link, and TabsContent slds-vertical-tabs__content.

Known gaps:

  • No icon pushed to the end of a vertical tab (slds-vertical-tabs__right-icon). An icon in TabsTrigger sits beside its label.
  • No overflow menu (slds-tabs__item_overflow), and the list doesn't scroll.
  • No size prop for slds-tabs_medium and slds-tabs_large.
  • SLDS has no disabled tab. A disabled tab here takes the theme's disabled text color.
  • Sub-tabs (slds-sub-tabs) and the unsaved and notification states (slds-is-unsaved, slds-has-notification) aren't covered.
PropValueSLDSNotes
TabsList variantdefaultslds-tabs_defaultA line under the list and a bar under the selected tab.
TabsList variantlineslds-tabs_defaultThe same as default. SLDS has one underlined style, so both values draw it.
TabsList variantscoped (added)slds-tabs_scoped
orientationverticalslds-vertical-tabsWith `variant="scoped"` on `TabsList`, SLDS vertical tabs: a box with the tab column beside the panel. With the default variant, a bar on the list's edge, which SLDS doesn't draw.

Keyboard interactions

Keys Action
Right Arrow Moves focus to the next tab, and selects it when tabs activate on focus. Wraps at the end.
Left Arrow Moves focus to the previous tab, and selects it when tabs activate on focus. Wraps at the start.
Down Arrow, Up Arrow With orientation="vertical", move to the next and previous tab. Left and Right do nothing.
Home, End Moves focus to the first or last tab, and selects it when tabs activate on focus.
Enter, Space Selects the focused tab, when the arrows only move focus.

Accessibility

  • Name the tab set with aria-label on TabsList, or aria-labelledby with the id of a visible heading.
  • The roles and states (tablist, tab with aria-selected and aria-controls, tabpanel) are set for you. Don't add them by hand.
  • Only the selected tab is in the Tab order; the arrows reach the rest. The active panel takes focus too, so a panel that holds only text is reachable.
  • A disabled tab can't be selected and is announced as unavailable. Say why in nearby text.
  • The selected tab doesn't rely on color alone: default tabs show a bar, and a scoped tab opens into its panel.

Motion

  • Tabs switch at once and the bar doesn't slide, so a change jumps, and reduced motion changes nothing. If you add a panel transition, put it behind motion-safe:.

Responsive behavior

  • The list fills its container. Tabs keep their width and never wrap or shrink, and there's no scrolling or overflow menu, so labels that don't fit run past the end of the list. Keep labels short and the count low, and test at the narrowest width you support.
  • Vertical scoped tabs keep a 15rem tab column at every width and wrap a long label onto a second line. Only the panel narrows, so in a narrow container use horizontal tabs.

Performance

  • Only the active panel renders its children, so a heavy panel costs nothing until someone opens it.
  • Switching tabs unmounts the old panel's children, so their state, like typed input, resets. Keep state that has to survive a switch above Tabs.

Rules

  • Every TabsTrigger needs a TabsContent with the same value, unique within one Tabs.
  • Always set defaultValue, or value with onValueChange, so the same tab opens in every project.
  • Don't wrap default tabs in your own border to imitate scoped tabs; use variant="scoped".
  • Don't restyle the bar, hover or active colors with classes. The --tabs-* tokens change per theme.
  • Mark an unavailable tab with disabled, not aria-disabled or pointer-events-none.
  • Keep TabsList and every TabsContent inside the same Tabs, with the panels after the list, and don't nest one tab set inside another's panel: variant and orientation styles match any ancestor Tabs, so they leak between levels.