Compound menu for switching between sections, with an animated active indicator.
Description
TabMenu is a compound component (TabMenu + TabMenuItem) that renders a <div role="group"> of real <button> items. The root tracks the active item and slides an animated line beneath it as the selection changes.
Reach for it when you have a small set of mutually exclusive sections to switch between. Typical surfaces: settings sidebars, dashboard section nav, in-page anchor menus, and routed nav rendered as links via asChild. Each item supports a leading icon, a label, a trailing icon, and arbitrary extra children (e.g. a count badge or a status dot).
The free build ships the horizontal-line layout with a sliding bottom indicator. Pro unlocks the full set of layout variants (vertical-button, vertical-line, horizontal-line, horizontal-button) and indicator positions (left, top, bottom).
If each option swaps a content panel within the same surface, use Tabs. For 2 to 5 inline, mutually exclusive options without rich item content, use SegmentedControl. For hierarchical path navigation, use Breadcrumb. For persistent full-app navigation, use Sidebar. For paged content controls, use Pagination.
Installation
Anatomy
TabMenu provides a context that hands size down to its items, plus an optional value binding. Each TabMenuItem is a self-contained <button> (or any element via asChild).
Usage
Examples
Richer previews (the full set of layout variants and indicator positions) are marked with a Pro badge. The live demo still runs for everyone; the source needs a Pro seat.
Variants
The free build ships the horizontal-line layout. Pro unlocks vertical-button (a sliding pill), vertical-line (icon over label), and horizontal-button, so the menu can match wherever it sits.
Indicators
The active item is marked by a sliding line. The free build draws it on the bottom (shown above). Pro adds a top line and a vertical left rail for the vertical layouts.
Sizes
Three sizes: sm, md (default), and lg. Padding, type, icon size, and item radius all scale together.
With Icon
Pass leading and/or trailing to any item. Icon sizing is driven by the active size, so you don't size the SVG yourself.
With Badge
Items accept arbitrary children alongside the label prop, which renders next to the label inside the item's content slot. Useful for unread counts, status dots, or "New" tags.
Disabled
Set disabled on an item to skip its click handler and apply the disabled visual treatment. The item also gets aria-disabled when rendered via asChild.
Controlled
Pass value and onValueChange to drive selection from your own state. Use uncontrolled defaultValue for simple in-page switchers; reach for controlled when other UI needs to react to the change (e.g. routing, persisting to URL).
As Link
Set asChild on each item and pass a next/link (or any anchor) as the child. The item forwards its slots and active state to the link, so the indicator animation and styling still work.
Accessibility
The root is a role="group". Each item is a real <button> (or the element passed via asChild) with aria-pressed reflecting the active state and aria-disabled when disabled. Every item is its own tab stop; there is no roving tabindex.
ARIA notes:
- The root sets
role="group". Pair it witharia-labeloraria-labelledbywhen the surrounding context doesn't already describe the menu. - Items set
aria-pressedtotrueorfalseso screen readers announce the toggle state. - In
asChildmode the underlying element (e.g.<a>) controls its own semantics;aria-pressedis omitted andaria-disabledis set whendisabledis passed. - Disabled items skip click handlers and
onValueChange. - If items only show an icon, add
aria-labelon each item so the button announces its purpose.
Styling
Tailwind override: pass className to merge Tailwind classes with the component's CVA classes (via cn()):
Data slots and attributes: the component sets these for CSS targeting.
data-slot="tab-menu"on the root element.data-slot="tab-menu-active-line"on the animated indicator line beneath the active item.data-slot="tab-menu-item"on each item.data-slot="tab-menu-item-icon"withdata-position="leading" | "trailing"on each icon wrapper.data-slot="tab-menu-item-content"on the inner content wrapper.data-slot="tab-menu-item-label"on the label wrapper.data-size="<size>"on both root and item.data-active="true" | "false"on each item, the active-state styling hook (it drives the active colors and the sliding indicator). Prefer this for styling: it survives even when the item doubles as a Radix overlay (Tooltip,Popover, …) trigger.data-state="on" | "off"mirrors the same state, but a Radix overlay overwritesdata-stateon its own trigger, so it is unreliable in that case.data-value="<value>"on each item.
Target the active item in CSS:
Related Components
- Tabs: use this when each option swaps a content panel within the same surface.
- SegmentedControl: use this for 2 to 5 inline mutually exclusive options without rich item content.
- Breadcrumb: use this for hierarchical path navigation.
- Sidebar: use this for persistent full-app navigation.
- Pagination: use this for paged content controls.
API Reference
TabMenu
Root that owns selection state and the size context. Extends React.ComponentProps<"div">, so standard div attributes (id, aria-label, aria-labelledby, etc.) are accepted.
Props
TabMenuItem
One item in the menu. Extends Omit<React.ComponentProps<"button">, "value">. Renders a <button> by default, or any element via asChild (Radix Slot).