Tabs connects each tab to a related content panel using a shared value. Keep values unique within a root. Use TabButton for route links and adjacent actions.
Usage
UsedefaultValue to set the initial selection. The root owns subsequent changes.
Tabs.Tab accepts startIcon, endIcon, badge, and a size of sm or md (default sm). The remaining props retain the installed Base UI contract. Values are not restricted to strings or numbers. Reference values must use the same identity on their tab, panel, and root. A root value of null selects no panel.
Anatomy
Place tabs and an optional indicator insideTabs.List. Place their panels alongside the list, all within the same Tabs.Root.
Give the list an accessible name and match each tab’s
value to its panel’s value. Base UI supplies the tab and panel IDs, aria-controls, and aria-labelledby relationships.
When you provide a custom id on Tabs.Panel, set aria-controls on the matching Tabs.Tab to that same ID. The installed Base UI 1.8.0 implementation requires this explicit relationship. Custom tab IDs are registered correctly for the panel’s aria-labelledby.
Add one Tabs.Indicator when you want the active underline. List does not insert an indicator.
Controlled state and event details
Passvalue and update it with onValueChange to own selection. The callback receives the next value and Base UI event details, including the originating event, reason, activationDirection, and cancel().
none. In uncontrolled roots, automatic fallback can have reason initial, disabled, or missing. Automatic fallback may select null when no enabled tab remains. These automatic changes cannot be canceled. Controlled roots keep selection under the application’s ownership.
Cancel a user change before updating controlled state, or use the same callback with an uncontrolled root:
Keyboard activation, orientation, and direction
Tabs.List uses manual activation by default (activateOnFocus={false}). Arrow keys move focus; Enter or Space activates the focused tab. Set activateOnFocus to select a tab when keyboard focus reaches it. Both policies work with controlled and uncontrolled state.
loopFocus defaults to true; set it to false to stop at either boundary.
Use DirectionProvider for RTL keyboard direction and dir="rtl" on the root for layout. Set orientation="vertical" on Root for a vertical list and indicator.
Panel lifetime
UsekeepMounted when panel contents must retain their state while inactive. Inactive mounted panels remain hidden and do not occupy layout space.
Panel layout
Panel fills the space left by the list and can shrink below its content on both axes, so it stays within a root of fixed height. To scroll its content, set overflow: auto on the panel, or on a child that fills it, such as a flex: 1 child of a column flex panel.
Native props, render composition, and refs
All five parts preserve native props, event handlers, refs, and Base UIrender composition. Their className and style can also be functions of the part’s state. The default elements are a div for Root, List, and Panel, a button for Tab, and a span for Indicator. Refs point to the actual rendered elements.
nativeButton consistent with the element composed for a tab. Use nativeButton={false} for a custom interactive element that is not a native button and forwards the supplied props and ref. A route anchor belongs in TabButton, where the route owns navigation and aria-current.
Visual emphasis
Usehighlighted on Tabs.Tab to outline its content independently of selection, such as when its settings are open. Selection remains owned by Tabs.Root.
Props
The generated reference includes the public props for every part. Public part state types and Root change detail/reason types are available fromtwenty-ui/primitives/navigation.
Tabs.Root
string | ((state: TabsRootState) => string | undefined)
CSS class applied to the element, or a function that
returns a class based on the component’s state.
any
default:"0"
The default value. Use when the component is not controlled.
When the value is
null, no Tab will be active.((value: any, eventDetails: TabsRootChangeEventDetails) => void)
Callback invoked when new value is being set.The event
reason is 'none' for user-initiated changes, such as a click
or keyboard navigation; 'initial' for the first automatic selection or
fallback in uncontrolled roots when defaultValue is omitted or
undefined, including when the implicit initial value is disabled or
missing; 'disabled' for automatic fallback when the selected tab becomes
disabled in uncontrolled roots; or 'missing' for automatic fallback when
the selected tab is removed, or when an explicit defaultValue never
matches a mounted tab in uncontrolled roots.For automatic changes, the selected value can be null when no enabled Tab
is available as a fallback.Automatic changes cannot be canceled; calling eventDetails.cancel() for
'initial', 'disabled', or 'missing' has no effect."horizontal" | "vertical"
default:"horizontal"
The component orientation (layout flow direction).
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, TabsRootState>
Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a
ReactElement or a function that returns the element to render.CSSProperties | ((state: TabsRootState) => CSSProperties | undefined)
Style applied to the element, or a function that
returns a style object based on the component’s state.
any
The value of the currently active
Tab. Use when the component is controlled.
When the value is null, no Tab will be active.Tabs.List
boolean
default:"false"
Whether to automatically change the active tab on arrow key focus.
Otherwise, tabs will be activated using <kbd>Enter</kbd> or <kbd>Space</kbd> key press.
string | ((state: TabsListState) => string | undefined)
CSS class applied to the element, or a function that
returns a class based on the component’s state.
boolean
default:"true"
Whether to loop keyboard focus back to the first item
when the end of the list is reached while using the arrow keys.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, TabsListState>
Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a
ReactElement or a function that returns the element to render.CSSProperties | ((state: TabsListState) => CSSProperties | undefined)
Style applied to the element, or a function that
returns a style object based on the component’s state.
Tabs.Tab
ReactNode
Content rendered after the label, such as a count.
string | ((state: TabsTabState) => string | undefined)
CSS class applied to the element, or a function that
returns a class based on the component’s state.
boolean
Whether the Tab is disabled.If a first Tab on a
<Tabs.List> is disabled, it won’t initially be selected.
Instead, the next enabled Tab will be selected.
However, it does not work like this during server-side rendering, as it is not known
during pre-rendering which Tabs are disabled.
To work around it, ensure that defaultValue or value on <Tabs.Root> is set to an enabled Tab’s value.ReactNode
Decorative content after the label and before the badge.
boolean
default:"false"
Emphasizes the tab content without changing selection.
boolean
default:"true"
Whether the component renders a native
<button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (for example, <div>).ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, TabsTabState>
Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a
ReactElement or a function that returns the element to render."md" | "sm"
default:"sm"
Visual size of the tab.
ReactNode
Icon rendered before the label.
CSSProperties | ((state: TabsTabState) => CSSProperties | undefined)
Style applied to the element, or a function that
returns a style object based on the component’s state.
any
required
The value of the Tab.
Tabs.Indicator
string | ((state: TabsIndicatorState) => string | undefined)
CSS class applied to the element, or a function that
returns a class based on the component’s state.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, TabsIndicatorState>
Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a
ReactElement or a function that returns the element to render.boolean
default:"false"
Whether to render itself before React hydrates.
This minimizes the time that the indicator isn’t visible after server-side rendering.
CSSProperties | ((state: TabsIndicatorState) => CSSProperties | undefined)
Style applied to the element, or a function that
returns a style object based on the component’s state.
Tabs.Panel
string | ((state: TabsPanelState) => string | undefined)
CSS class applied to the element, or a function that
returns a class based on the component’s state.
boolean
default:"false"
Whether to keep the HTML element in the DOM while the panel is hidden.
ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, TabsPanelState>
Allows you to replace the component’s HTML element
with a different tag, or compose it with another component.Accepts a
ReactElement or a function that returns the element to render.CSSProperties | ((state: TabsPanelState) => CSSProperties | undefined)
Style applied to the element, or a function that
returns a style object based on the component’s state.
any
required
The value of the TabPanel. It will be shown when the Tab with the corresponding value is active.
Migration
Add an explicitTabs.Indicator to lists that should retain the active underline. Remove duplicate indicators from compositions that previously relied on List inserting one. Keep route links, create controls, and overflow controls outside Tabs.List, whose children select related panels.