A React tabs component with a Liquid Glass tab list, animated active indicator, and keyboard navigation.
Tabs switches between related panels on the same page. The tab list sits on a shared glass surface, and a translucent lens follows the active tab.
Tabs are passed through the items array. Each item's value links its tab button to the matching panel, while content renders inside that panel. Use defaultValue for uncontrolled usage or value with onValueChange for a controlled component.
Labels and icons share the tab button. The first enabled tab is active by default.
Supports xs / sm / md / lg / xl, defaulting to md. Tab height, padding, text, icons, spacing, and panel text scale together.
value and onValueChange let external state fully control the active tab. Set value to null when no tab should be active.
Active tab: Overview
Use orientation="vertical" for a side-by-side layout. Arrow keys follow the orientation, so ArrowDown and ArrowUp move through vertical tabs.
Set disabled on the component to disable every tab, or on a single item to disable only that tab. Disabled tabs stay visible and remain part of the tab list.
Panels unmount when another tab becomes active. Set keepMounted on an item to keep its panel in the DOM so local state survives tab switches.
By default, arrow keys move focus and Enter or Space activates the tab. Set activateOnFocus to switch panels as focus moves.
Active tab: Overview. Focus the list and press an arrow key to switch without pressing Enter.
Tabs are named by their label. When a tab renders only an icon, give it an aria-label or aria-labelledby.
Tabs renders a tablist of tab buttons and linked tabpanels. The active tab exposes aria-selected, and each panel is connected with aria-controls and aria-labelledby.
Arrow keys move focus along the orientation, Home and End jump to the ends, and Enter or Space activates the focused tab when activateOnFocus is false. Name the tab list with aria-label or aria-labelledby; icon-only tabs need their own name. See the accessibility guide for keyboard details.
Tabs
| Prop | Type | Default | Description |
|---|---|---|---|
items | readonly TabsItemProps[] | — | Tabs and panels rendered by the component. |
value | any | — | Controlled active value. Use null to render no active tab. |
defaultValue | any | First enabled item | Initial active value when uncontrolled. |
onValueChange | (value: any, details) => void | — | Called when the active value changes. |
orientation | "horizontal" | "vertical" | "horizontal" | Layout and keyboard orientation. |
activateOnFocus | boolean | false | Activates a tab as arrow-key focus moves. |
loopFocus | boolean | true | Loops keyboard focus back to the first tab after the last one. |
disabled | boolean | false | Disables every tab. |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Tab height, padding, text, icons, and spacing. |
aria-label | string | — | Accessible name for the tab list. |
aria-labelledby | string | — | ID of an external element that names the tab list. |
aria-describedby | string | — | ID of external text that describes the tab list. |
render | ReactElement | ((props, state) => ReactElement) | — | Replaces the root element. |
ref | Ref<HTMLDivElement> | — | Ref forwarded to the root element. |
className | string | — | Tailwind classes merged into the root. |
style | CSSProperties | — | Inline styles merged into the same element as className. |
ui | { root?: ClassValue; list?: ClassValue; glass?: ClassValue; overlay?: ClassValue; indicator?: ClassValue; tab?: ClassValue; icon?: ClassValue; label?: ClassValue; panels?: ClassValue; panel?: ClassValue; panelContent?: ClassValue } | — | Tailwind classes for internal slots, keyed by data-slot. |
TabsItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | any | — | Value that links the tab to its panel. |
label | ReactNode | — | Visible label inside the tab. |
icon | ReactNode | — | Decorative icon rendered before the label. |
content | ReactNode | — | Panel content shown while the tab is active. |
keepMounted | boolean | false | Keeps the panel in the DOM while another tab is active. |
disabled | boolean | false | Disables this tab. |
aria-label | string | — | Accessible name for an icon-only tab. |
aria-labelledby | string | — | Names an icon-only tab from another element. |
nativeButton | boolean | true | Whether the tab renders a native <button>. |
render | ReactElement | ((props, state) => ReactElement) | — | Replaces the tab element. |
ref | Ref<HTMLElement> | — | Ref forwarded to the tab element. |
className | string | — | Tailwind classes merged into the tab button. Use ui.panel for the panel. |
style | CSSProperties | — | Inline styles merged into the same element as className. |
ui | { tab?: ClassValue; icon?: ClassValue; label?: ClassValue; panel?: ClassValue; panelContent?: ClassValue } | — | Tailwind classes for this tab's internal slots, keyed by data-slot. |
Root ui is merged into every tab and panel first, then the item's ui. The item's className applies only to the tab button; style the panel through ui.panel.
| Attribute | Description |
|---|---|
data-active | Present on the active tab. |
data-disabled | Present when the component or tab is disabled. |
data-orientation | Indicates the orientation (horizontal or vertical). |
data-activation-direction | Indicates the direction of the newly active tab relative to the previous one. |
data-hidden | Present on a mounted panel that is not active. |
data-starting-style | Present when a panel begins animating in. |
data-ending-style | Present when a panel is animating out. |
data-slot | Marks internal parts (root, list, glass, overlay, indicator, tab, icon, label, panels, panel, panel-content). |
The component uses the shared glass material. Tune shared --ui-glass-* tokens on a parent, use className for layout, and use ui for internal slots keyed by data-slot. Component-specific values live in src/theme/tabs.ts. See the theming guide for token layers and presets.