Compound menu for switching between sections, with vertical or horizontal layouts and 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 renders one of two animated indicators behind the scenes: a background pill that slides between items (the vertical-button and horizontal-button variants), or a line drawn on the side, top, or bottom of the active item (the vertical-line, horizontal-line, and vertical-button + left combinations).
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).
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 variant, size, and indicator down to its items, plus an optional value binding. Each TabMenuItem is a self-contained <button> (or any element via asChild).
Usage
Examples
Variants
variant picks the layout and the indicator style. vertical-button and horizontal-button render a sliding background pill; vertical-line and horizontal-line draw a line on the active item's edge.
Sizes
Three sizes: sm, md, and lg (default). Padding, type, icon size, and item radius all scale together.
Indicators
indicator is the placement of the active-item line. The intentional pairings are vertical-button with left, vertical-line with top or bottom, and horizontal-line with top or bottom. horizontal-button does not use an indicator. Other combinations render without breaking, but the result is not designed for.
With Icon
Pass leadingIcon and/or trailingIcon 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-bg"on the sliding background pill (rendered forvertical-buttonandhorizontal-buttonvariants).data-slot="tab-menu-active-line"on the animated indicator line (rendered whenindicatoris set toleft,top, orbottom).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-variant="<variant>",data-size="<size>",data-indicator="<indicator>"on both root and item.data-state="on" | "off"anddata-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 provides the variant context. Extends React.ComponentProps<"div">, so standard div attributes (id, aria-label, aria-labelledby, etc.) are accepted.
Props
Variants
TabMenuItem
One item in the menu. Extends Omit<React.ComponentProps<"button">, "value">. Renders a <button> by default, or any element via asChild (Radix Slot).