Tabs
betaA row of tabs that switches between panels in one place, with a sliding indicator, a filled or underlined look, tones, and sizes.
Preview
solid
ghost
size sm
size md
size lg
icons
disabled
scroll
vertical solid
vertical ghost
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/tabs
Use it for
- Splitting one page's content into views people switch between, like Overview, Activity, and Settings.
- Give each TabsTab and its TabsPanel the same value. Put each TabsTab directly inside TabsList.
- Always set defaultValue on Tabs to an enabled tab. The server HTML then shows the right tab and panel.
- Use the default solid look for a compact switcher. Use emphasis='ghost' for an underlined row across the top of a section.
- A row too long for its space scrolls sideways. Each edge with more tabs past it fades out, so people can tell.
- Opening a tab in a long row scrolls the row so that tab sits clear of the fades. A click, a new value, and a defaultValue far along the row all do this.
- Set orientation='vertical' on Tabs for a list beside the panels. The arrow keys then move up and down.
- The arrow keys open each tab they reach. Set activateOnFocus={false} on TabsList when a panel is slow to show, such as one that loads data, so people can pass tabs without opening each one.
- Give TabsList an aria-label when a page has more than one tab list.
Avoid it for
- Moving between pages. Use links in a nav, because tabs do not change the URL.
- Steps people must finish in order. Show one step at a time with its own buttons.
- Picking a value that a form sends. Use a radio group or a select.
- Wrapping a TabsTab in another element. Its look comes from TabsList, its direct parent.
Props
- Tabs
The root. Holds the open tab's value and stacks the list above the panels.
TabsProps
- TabsList
Part. The row of tabs, with the sliding indicator built in. Takes tone, emphasis, and size.
TabsListProps
- TabsTab
Part. One tab button. Needs a value that matches a panel.
TabsTabProps
- TabsPanel
Part. The content for the tab with the same value. Only the open panel is in the page, unless keepMounted is set.
TabsPanelProps
- defaultValue
On Tabs. The tab that starts open. Set it, or the server HTML may show no open tab.
string | number
- value
Required on TabsTab and TabsPanel, where matching values link a tab to its panel. On Tabs, it controls the open tab. Use it with onValueChange.
string | number
- onValueChange
On Tabs. Called with the new value when someone opens a tab.
(value, details) => void
- orientation
On Tabs. Vertical puts the list beside the panels, and the arrow keys move up and down.
"horizontal" | "vertical" · default "horizontal"
- tone
On TabsList. Color of the active tab's pill or line.
"neutral" | "brand" · default "neutral"
- emphasis
On TabsList. Solid puts the active tab on a filled pill over a muted track. Ghost underlines it.
"solid" | "ghost" · default "solid"
- size
On TabsList. The list is as tall as a control of the same size. Density scales it.
"sm" | "md" | "lg" · default "md"
- activateOnFocus
On TabsList. Opens a tab as soon as an arrow key reaches it. Set it to false for panels that are slow to show, and Enter or Space then opens the focused tab.
boolean · default true
- disabled
On TabsTab. Blocks opening that tab, and dims it. The arrow keys still reach it.
boolean · default false
- keepMounted
On TabsPanel. Keeps the panel in the page while it is hidden, so its state survives a switch.
boolean · default false
- className
Extra classes on any part. Later classes win, so className='[--tabs-radius:0px]' on TabsList squares the corners, and '[--tabs-fade:0px]' turns off the edge fades.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| Tabs | TabsProps | - | The root. Holds the open tab's value and stacks the list above the panels. |
| TabsList | TabsListProps | - | Part. The row of tabs, with the sliding indicator built in. Takes tone, emphasis, and size. |
| TabsTab | TabsTabProps | - | Part. One tab button. Needs a value that matches a panel. |
| TabsPanel | TabsPanelProps | - | Part. The content for the tab with the same value. Only the open panel is in the page, unless keepMounted is set. |
| defaultValue | string | number | - | On Tabs. The tab that starts open. Set it, or the server HTML may show no open tab. |
| value | string | number | - | Required on TabsTab and TabsPanel, where matching values link a tab to its panel. On Tabs, it controls the open tab. Use it with onValueChange. |
| onValueChange | (value, details) => void | - | On Tabs. Called with the new value when someone opens a tab. |
| orientation | "horizontal" | "vertical" | "horizontal" | On Tabs. Vertical puts the list beside the panels, and the arrow keys move up and down. |
| tone | "neutral" | "brand" | "neutral" | On TabsList. Color of the active tab's pill or line. |
| emphasis | "solid" | "ghost" | "solid" | On TabsList. Solid puts the active tab on a filled pill over a muted track. Ghost underlines it. |
| size | "sm" | "md" | "lg" | "md" | On TabsList. The list is as tall as a control of the same size. Density scales it. |
| activateOnFocus | boolean | true | On TabsList. Opens a tab as soon as an arrow key reaches it. Set it to false for panels that are slow to show, and Enter or Space then opens the focused tab. |
| disabled | boolean | false | On TabsTab. Blocks opening that tab, and dims it. The arrow keys still reach it. |
| keepMounted | boolean | false | On TabsPanel. Keeps the panel in the page while it is hidden, so its state survives a switch. |
| className | string | - | Extra classes on any part. Later classes win, so className='[--tabs-radius:0px]' on TabsList squares the corners, and '[--tabs-fade:0px]' turns off the edge fades. |
Accessibility
- TabsList has role="tablist", each TabsTab has role="tab" with aria-selected, and each panel has role="tabpanel" named by its tab.
- The arrow keys move between tabs, open each tab they reach, and wrap at the ends. Home and End jump to the first and last tab. With activateOnFocus={false}, the keys only move focus, and Enter or Space opens the focused tab.
- A disabled tab has aria-disabled. The arrow keys still stop on it, so screen reader users hear that it exists, but it does not open.
- Only the active tab is in the Tab order, and the list itself is not, even when it scrolls. Tab then moves into the open panel.
- Inactive labels use muted-foreground, which the theme tests hold to 4.5:1 on the muted track and on the page.
- The solid look's active label uses the tone's foreground on the tone, and the ghost look's uses the tone's soft foreground on the page. The theme tests hold both to 4.5:1.
- The indicator slides with --motion-duration-base and --motion-ease-out. It still slides under reduced motion.
- A small inline script places the indicator before React hydrates. Without JavaScript, the solid look's active tab fills itself, so its label stays readable.
- In a row that scrolls, the arrow keys bring the focused tab and its ring fully clear of the edge fades. Forced-colors mode turns the fades off.
- A click or a new value brings the open tab clear of the fades too, and tabs that come or go keep it clear while the row stays where it last scrolled to the open tab. Once the row has been hidden and shown again, that no longer holds: a tab added past the open tab can leave it partly under the right fade. The row scrolls at once, with no animation, and the page does not move.
- A press opens its tab once it lifts on that tab, so a tab never opens or scrolls away under a held button. A press dragged off the tab opens nothing.
- The fades and that scroll start once React runs. Until then, and without JavaScript, a long row shows its first tabs, even when the open tab is further along. Its edges are cut off with no fade.
Examples
Settings sections
<Tabs defaultValue="account">
<TabsList aria-label="Settings">
<TabsTab value="account">Account</TabsTab>
<TabsTab value="password">Password</TabsTab>
<TabsTab value="billing">Billing</TabsTab>
</TabsList>
<TabsPanel value="account">Change your name and email.</TabsPanel>
<TabsPanel value="password">Change your password.</TabsPanel>
<TabsPanel value="billing">See your plan and invoices.</TabsPanel>
</Tabs>Underlined, in the brand color
<Tabs defaultValue="overview">
<TabsList emphasis="ghost" tone="brand" aria-label="Project">
<TabsTab value="overview">Overview</TabsTab>
<TabsTab value="activity">Activity</TabsTab>
<TabsTab value="settings">Settings</TabsTab>
</TabsList>
<TabsPanel value="overview">Overview content</TabsPanel>
<TabsPanel value="activity">Activity content</TabsPanel>
<TabsPanel value="settings">Settings content</TabsPanel>
</Tabs>Beside the panels
<Tabs defaultValue="general" orientation="vertical">
<TabsList emphasis="ghost" aria-label="Preferences">
<TabsTab value="general">General</TabsTab>
<TabsTab value="alerts">Alerts</TabsTab>
<TabsTab value="privacy">Privacy</TabsTab>
</TabsList>
<TabsPanel value="general">General settings</TabsPanel>
<TabsPanel value="alerts">Alert settings</TabsPanel>
<TabsPanel value="privacy">Privacy settings</TabsPanel>
</Tabs>Controlled
"use client";
import { useState } from "react";
function ReportTabs() {
const [tab, setTab] = useState("week");
return (
<Tabs value={tab} onValueChange={setTab}>
<TabsList size="sm" aria-label="Report range">
<TabsTab value="week">Week</TabsTab>
<TabsTab value="month">Month</TabsTab>
<TabsTab value="year">Year</TabsTab>
</TabsList>
<TabsPanel value={tab}>Showing the last {tab}.</TabsPanel>
</Tabs>
);
}