Tabs

Switch between peer views inside one screen: Tabs, TabList, Tab, and TabPanel. Six types across two orientations, no scrolling of their own.

Demo

The controls sit on TabList, where the kit puts every visual decision — except orientation, which lives on the root. Flip it to vertical and underline gives way to line.

orientation
type
size
import { Lock01, User01, Users01 } from "@untitledui/icons"; import { Tabs } from "@/components/application/tabs/tabs"; <Tabs> <Tabs.List aria-label="Settings" type="button-brand"> <Tabs.Item id="details" icon={User01}>My details</Tabs.Item> <Tabs.Item id="password" icon={Lock01}>Password</Tabs.Item> <Tabs.Item id="team" icon={Users01} badge={4}>Team</Tabs.Item> </Tabs.List> </Tabs>

Three components, one bar

Tabs owns the selection, Tabs.List owns the look, and Tabs.Item owns one label. The copied snippet carries all three, because no single element reproduces what the preview shows.

Contextual demo

Tabs only make sense next to the thing they switch: four bars in a real screen, each wired to real panels.

This page is also a consumer

The bar you used to get here is this component: type="button-brand" at size="sm", rendered by the docs page shell. The three tabs are the panels, and ?tab= in the address bar is the selected key.

The family

Four exports, also reachable as Tabs.List, Tabs.Item, and Tabs.Panel off the root.

The components in this family, with their descriptions and import paths
ComponentDescriptionImport from
TabsThe root. Owns the selected key, the orientation, and the keyboard model, and lays the list and the panels out in a column horizontally, a row vertically.@/​components/​application/​tabs/​tabs
TabListThe bar itself: every visual decision lives here, since size, type, and fullWidth are its props rather than the root’s.@/​components/​application/​tabs/​tabs
TabOne tab. Takes a label, an optional leading icon, and an optional badge.@/​components/​application/​tabs/​tabs
TabPanelThe content for one tab, matched to it by id. It becomes a tab stop of its own when it holds nothing focusable.@/​components/​application/​tabs/​tabs

Built from

Four pieces make up the bar; one has its own docs page.

Component
Where it’s used here
BadgeThe badge prop, at size sm, as pill-color on the brand, underline, and line types and modern everywhere else. On those three the color is brand only while the tab is selected or hovered and gray the rest of the time; the modern badge is always gray.

The variant space

Five horizontal types at both sizes — the ten Desktop bars the sheet draws — all live. Wide bars scroll in narrow cells.

size sm
button-brand
button-gray
button-border
button-minimal
underline
size md
button-brand
button-gray
button-border
button-minimal
underline
fullWidth
button-border, fullWidth
underline, fullWidth

Size moves everything at once

Measured in the browser: a button tab is 36px tall at sm and 44px at md, an underline tab 32px then 36px, with the label at 14px then 16px and the icon at 16px then 20px. Every one of those matches the sheet symbol beside it.

Vertical

The same five types at both sizes with underline swapped for line — the ten vertical Desktop rails the sheet draws. Underline belongs to a horizontal bar, line to a vertical one.

size sm
button-brand
button-gray
button-border
button-minimal
line
size md
button-brand
button-gray
button-border
button-minimal
line

Orientation belongs on Tabs

TabList no longer takes an orientation prop (deprecated 2026-08-21, removed 2026-08-22): it only ever styled the list. Arrow-key direction, the data-orientation attribute and the list’s own layout all come from the root, so set it on Tabs and the list follows. The trade-off is that the type union no longer narrows, so the compiler will let a vertical bar take underline. Pair them yourself.

States

Selection, hover, and focus are the sheet’s three. Disabled is React Aria’s fourth, painted only as an opacity change.

The panel for Password. Selection starts here, and the panel below the bar changes with it.
Selected, hover, and focus are all live: tab into the bar, then use the arrow keys
Disabled: Password is in disabledKeys and matches Team at rest. Hover both to see the one difference

A disabled tab is dimmed, not explained

getTabStyles reads isDisabled and paints it at half opacity with a not-allowed cursor, or in the system’s GrayText under forced colors. It also stays flat on hover, since React Aria passes isDisabled into useHover, so isHovered never turns true while its neighbors light up under the pointer. The opacity tells a reader something is off; it does not say what. The sheet draws no disabled symbol either way.