Tabs
A set of layered sections of content—known as tab panels—that are displayed one at a time.
Installation
npx shadcn@latest add @redpanda/tabspnpm dlx shadcn@latest add @redpanda/tabsyarn dlx shadcn@latest add @redpanda/tabsbunx shadcn@latest add @redpanda/tabsnubx shadcn@latest add @redpanda/tabsaube dlx shadcn@latest add @redpanda/tabsThe registry ships two visual styles for TabsList and TabsTrigger: the default segmented pill and the underline bar. Most previews on this page show both inline so you can compare them at a glance.
Default
Underline
Live appearance specimens
Selected, inactive and unavailable states use the component’s actual behavior, not painted hover overrides. Hover an inactive tab, or move focus with the arrow keys and activate it with Enter or Space.
Variants
Card
Use the card variant for tabs with a bordered card container.
Default
Underline
Contained
Use the contained variant for tabs with a muted background container.
Sizes
Small
Use size="sm" for compact tabs.
Default
Underline
Large
Use size="lg" for larger tabs.
Default
Underline
Layout
Equal Layout
Use layout="equal" with columns to create equal-width tabs.
Default
Underline
Full Layout
Use layout="full" to make tabs span the full width.
Default
Underline
Gap
Use the gap prop to control spacing between tabs.
Default
Underline
Scrollable
When a horizontal tab strip has more triggers than fit the available width, use ScrollableTabsList
in place of TabsList. The strip stays on one line and becomes drag-scrollable instead of wrapping:
edge fades signal hidden tabs, a drag won’t accidentally activate a tab, and keyboard arrow
navigation still scrolls the focused tab into view.
import { ScrollableTabsList, Tabs, TabsTrigger } from '@/components/tabs';
<Tabs defaultValue="overview"> <ScrollableTabsList variant="underline">{/* many triggers… */}</ScrollableTabsList> {/* panels… */}</Tabs>;ScrollableTabsList is a thin convenience wrapper: a TabsList pre-composed with
DragScrollArea that supplies the content sizing (w-max min-w-full) and,
for the underline variant, a native content inset that protects the overhanging underline. The scroll viewport owns its masks and drag behavior; the bottom 4px remains opaque so the baseline stays crisp. If you need more control, compose
DragScrollArea around a plain TabsList directly — the wrapper just saves you that boilerplate.
This works with any variant. The edge fade is an alpha mask over the strip itself, so it blends on
any background. It pairs most naturally with underline (a nav-style strip), but the default
segmented variant scrolls too:
Combinations
Contained with Equal Layout
Combine contained variant with equal layout.
Combined with Toggle Group
This example demonstrates the proper relationship between tabs and toggle groups. Use tabs to navigate between distinct content areas, and toggle groups within each tab to filter or adjust that specific view’s content.
Patterns
Keyboard activation
Tabs use manual keyboard activation by default: arrow keys move focus, then Enter or
Space activates the focused tab. This avoids triggering navigation or data fetching while someone is only exploring the tab list.
Set activateOnFocus when every panel is available immediately and switching panels has no costly
side effects:
<TabsList activateOnFocus>{/* triggers */}</TabsList>Vertical orientation
Underline triggers use an inset 3px keyboard focus ring so a horizontal scrollport cannot clip its top and bottom edges. Default segmented triggers retain their exterior ring.
Vertical tabs use the same standard segmented-pill recipe as horizontal tabs. Use size="xl" for the 16px list-to-panel gap, rather than overriding root spacing. Orientation supplies the column layout and left-aligned triggers.
Disabled trigger with tooltip
Disabled tabs remain keyboard-focusable for discoverability, matching Base UI. Focus does not select them, and Enter or Space cannot activate them.
Default
Underline
Count badges
Default
Underline
Icon-only triggers
Default
Underline
As links
Pass a JSX element (or render function) to TabsTrigger’s render prop to swap the rendered element while keeping
keyboard, data-state, and active-highlight behavior intact.
Default
Bind Tabs.value to your router’s active route when using router Link components.
The native-anchor examples below use real, per-instance panel fragments and keep
selection synchronized with browser Back and Forward. Their generated instance IDs
are not permanent application routes.
Underline
<TabsList> <TabsTrigger render={<Link to="/overview" />} value="overview"> Overview </TabsTrigger> <TabsTrigger render={<Link to="/billing" />} value="billing"> Billing </TabsTrigger></TabsList>When to use
Tabs function as a navigation bar positioned atop content, allowing users to switch between distinct content views. Each tab displays unique content specific to that tab, without duplication across tabs. When a user interacts with a tab, it triggers the display of new content in a fresh view.
Use tabs when:
- Navigating between different, self-contained content areas
- Each section has unique content specific to that context
- Content changes completely when switching tabs
- Users need to switch between parallel information hierarchies
Don’t use tabs when:
- Filtering or adjusting a single content view (use Toggle Group/Segmented Control instead)
- Content is shared or filtered across different states
- You need to show/hide portions of the same dataset
Usage
<Tabs> <TabsList> <TabsTrigger value="tab1">Tab 1</TabsTrigger> <TabsTrigger value="tab2">Tab 2</TabsTrigger> </TabsList> <TabsContents> <TabsContent value="tab1">Tab 1 Content</TabsContent> <TabsContent value="tab2">Tab 2 Content</TabsContent> </TabsContents></Tabs>Underline Variant
<Tabs> <TabsList variant="underline"> <TabsTrigger variant="underline" value="tab1"> Tab 1 </TabsTrigger> <TabsTrigger variant="underline" value="tab2"> Tab 2 </TabsTrigger> </TabsList> <TabsContents> <TabsContent value="tab1">Tab 1 Content</TabsContent> <TabsContent value="tab2">Tab 2 Content</TabsContent> </TabsContents></Tabs>Props
Registry Props
Tabs
size?'sm' | 'md' | 'lg' | 'xl' | 'full'
The size of the tabs container
'sm' | 'md' | 'lg' | 'xl' | 'full''md'variant?'default' | 'card' | 'contained'
The visual variant of the tabs container
'default' | 'card' | 'contained''default'TabsList
variant?'default' | 'underline'
The visual variant of the tabs list
'default' | 'underline''default'layout?'auto' | 'equal' | 'full'
The layout of the tabs list
'auto' | 'equal' | 'full''auto'gap?'none' | 'sm' | 'md' | 'lg'
The gap between tabs
'none' | 'sm' | 'md' | 'lg''none'columns?number
Number of columns for equal layout
numberactiveClassName?string
The class name to apply to the active tab highlight (e.g. to customize its CSS transition)
stringactivateOnFocus?boolean
Whether focusing a tab with arrow keys also activates it
booleanfalseTabsTrigger
variant?'default' | 'underline'
The visual variant of the tabs trigger
'default' | 'underline''default'TabsContents
childrenReact.ReactNode
The children of the tabs contents
React.ReactNodeclassName?string
The class name to apply to the tabs contents
stringKeyboard interactions
| Key | Action |
|---|---|
Tab into TabsList |
Focus lands on the active tab. |
ArrowLeft / ArrowRight |
Moves focus between tabs (horizontal orientation). |
ArrowUp / ArrowDown |
Moves focus between tabs (vertical orientation). |
Home / End |
Jumps to first / last tab. |
Space / Enter |
Activates the focused tab in the default manual mode. |
Accessibility
TabsListcarriesrole="tablist", eachTabsTriggerisrole="tab"witharia-selected, and eachTabsPanelisrole="tabpanel"wired viaaria-controls/aria-labelledby.- Focus is roving inside
TabsList— only the active tab is in the tab order; the others are reachable via arrow keys. - Tabs activate manually by default; pass
activateOnFocustoTabsListonly when every panel is immediately available. - When wrapped in
DragScrollArea, arrow-key navigation scrolls the focused tab into view, so all tabs stay keyboard-reachable even while some are visually clipped. - Use the
underlinevariant sparingly when tabs live next to inline links — the underline can be mistaken for a link.