Tabs
Layered sections of content, called tab panels, that show one at a time.
asChild with Links
Account
Make changes to your account here. Click save when you're done.
Composition
Compose the parts of Tabs together to build your own:
Polymorphism
Tabs.Trigger accepts an asChild prop. When true, it composes the trigger styling and tab behavior onto its single child instead of rendering its own <button>. Use it to render each tab as a router link (a react-router Link here) so tabs drive client-side navigation while keeping the active-tab styling.
A link tab activates on click, not on the press, so a press released off the link switches no tab and follows no link.
Tabs.Separator accepts asChild too. When true, it composes the hairline's styling and attributes onto your own element instead of rendering its own <div>.
Tabs.Root, Tabs.List, and Tabs.Content accept asChild through Radix. Each composes its part's attributes and behavior onto your own element instead of rendering its own <div>.
Horizontal overflow
When there are too many tabs to fit the container, the list scrolls horizontally with scroll-position-aware fade shadows at the edges.
Pill appearance
Use appearance="pill" to render each tab as a pill instead of the default classic underline style.
asChild with Links
Account
Make changes to your account here. Click save when you're done.
Separator
Tabs.List draws no border of its own. Compose Tabs.Separator directly after it for the 1px hairline in the separator color: under the list when the root is horizontal, beside the list when the root is vertical. The separator is a sibling of the list and not a child, so it spans the root's full width and neither scrolls nor fades with the triggers.
As the root's direct child, the separator pulls itself up by --tabs-gap, the root's gap, and sits flush against the list while the content keeps the gap. Inside a wrapper of your own (a page header that holds a title, badges, and the list), the offset stays off and the separator sits where the wrapper lays it out. To change the space between the list and the content, set --tabs-gap on Tabs.Root. A gap-* class changes the gap alone and leaves the separator's offset at 1rem.
The separator is decorative (role="none"). Pass semantic to render role="separator".
with Tabs.Separator
without
vertical
The separator runs beside the list, the full height of the root.
Accessibility
Tabs follows the WAI-ARIA tabs pattern through Radix: the list is a role="tablist", each trigger is a role="tab" with aria-selected and aria-controls, and each content panel is a role="tabpanel" labelled by its tab.
- Tab moves focus into the list and lands on the active tab. Only the active tab is a Tab stop, so the next Tab leaves the list. This holds for
asChildtriggers too, so a list of router links is one Tab stop, not one per link. - ArrowLeft / ArrowRight (or ArrowUp / ArrowDown when vertical) move focus between tabs and activate the focused tab. Home and End jump to the first and last tab.
- When a horizontal list overflows, keyboard focus scrolls the focused tab to the center of the visible area. A pointer click never scrolls the list, so the tab you press stays under the pointer.
- A
disabledtrigger carriesaria-disabled. WithasChild, it also drops the child'shref, so a disabled link cannot navigate.
Data attributes
API Reference
The Tabs components accept the following props.
Tabs.Root
Layered sections of content, called tab panels, that show one at a time. The outermost part; it owns orientation and appearance. It stamps both as data-orientation and data-appearance for the parts below to style against. Based on Radix Tabs Root.
CSS variables
Data attributes
Tabs.List
Contains the triggers that are aligned along the edge of the active content. Based on Radix Tabs List.
The list draws no border of its own. Compose Tabs.Separator after it for the hairline between the triggers and the content.
Data attributes
Tabs.Separator
The hairline between the list and the content, in the separator color. Compose it directly after Tabs.List. It follows the root's orientation: under the list when horizontal, beside the list when vertical. It is a sibling of the list and not a child, so it spans the root's full width and neither scrolls nor fades with the triggers. As the root's direct child it pulls itself up by --tabs-gap and sits flush against the list. Inside a wrapper of your own, the offset stays off. Composes the mantle Separator.
CSS variables
Data attributes
Tabs.Trigger
The button that activates its associated content. Based on Radix Tabs Trigger.
children render inside a <span data-slot="tabs-trigger-label">, on the plain path and the asChild path alike. The active-tab decoration is a permanent element sibling, so without the span a bare text label is never a lone child: a browser translation engine reparents that text node, and the removal React runs when the label changes shape or goes away throws. The span is display: contents, so the icon, the label, and Tabs.Badge stay flex items of the trigger and keep its gap. Browser translation covers the mechanism and the rules your own components need.
An icon you pass as a child is now a grandchild of the trigger, so a [&>svg] class of your own no longer reaches it. Match the part's own variant instead: [&>[data-slot=tabs-trigger-label]>svg]:size-4. A [&_svg] class or a class on the icon itself loses to the trigger's default, which carries one more attribute selector.
Data attributes
Tabs.Content
Contains the content associated with each trigger. Based on Radix Tabs Content.
Data attributes
Tabs.Badge
A badge to render inside a tab trigger, typically a count or a status indicator.