Tabs
A set of tab buttons that switch between related panels on the same page.
API Reference
Original primitive API
Behavior, accessibility details, and low-level props are documented by Base UI.
Basic
import { Tabs, TabsList, TabsPanel, TabsTab,} from "moduix";export function TabsDemo() { return ( <Tabs defaultValue="overview"> <TabsList> {items.map((item) => ( <TabsTab key={item.value} value={item.value}> {item.title} </TabsTab> ))} </TabsList> {items.map((item) => ( <TabsPanel key={item.value} value={item.value}> {item.content} </TabsPanel> ))} </Tabs> );}const items = [ { value: "overview", title: "Overview", content: "Review project status, team velocity, workloads and activity highlights in one place.", }, { value: "projects", title: "Projects", content: "Track active workstreams, owners and milestones across all departments.", }, { value: "account", title: "Account", content: "Manage personal settings, team settings, notifications and access preferences.", },];Full list of component variables available for project-level overrides.
| Property | Default | Description |
|---|---|---|
| --tabs-bg | var(--color-background) | Controls the root background color. |
| --tabs-border-color | var(--color-border) | Controls the root border color. |
| --tabs-border-width | var(--border-width-sm) | Controls the root border width. |
| --tabs-color | var(--color-foreground) | Controls the root text color. |
| --tabs-focus-ring-color | var(--color-ring) | Controls tab and panel focus ring color. |
| --tabs-focus-ring-offset | 0 | Controls tab focus ring offset. |
| --tabs-focus-ring-width | var(--border-width-sm) | Controls tab and panel focus ring width. |
| --tabs-gap | 0 | Controls spacing between the tab list and panels. |
| --tabs-indicator-bg | var(--color-background) | Controls the default indicator background. |
| --tabs-indicator-radius | var(--radius-sm) | Controls the default indicator radius. |
| --tabs-indicator-size | 1.75rem | Controls the default indicator thickness. |
| --tabs-indicator-transition | translate 200ms ease, width 200ms ease | Controls the default indicator movement transition. |
| --tabs-line-indicator-bg | var(--color-foreground) | Controls the line indicator color. |
| --tabs-line-indicator-radius | var(--radius-full) | Controls the line indicator radius. |
| --tabs-line-indicator-size | 2px | Controls the line indicator thickness. |
| --tabs-line-indicator-transition | translate 200ms ease, width 200ms ease | Controls the line indicator movement transition. |
| --tabs-list-bg | var(--color-muted) | Controls the tab list background color. |
| --tabs-list-border-color | var(--color-border) | Controls the tab list separator color. |
| --tabs-list-border-width | var(--tabs-border-width, var(--border-width-sm)) | Controls the tab list separator width. |
| --tabs-list-gap | 0.25rem | Controls spacing between tabs. |
| --tabs-list-padding | 0.25rem | Controls the tab list padding. |
| --tabs-list-padding-x | 0.25rem | Controls the tab list horizontal padding. |
| --tabs-list-padding-y | 0.25rem | Controls the tab list vertical padding. |
| --tabs-max-width | calc(100vw - 2rem) | Controls the root tabs max width. |
| --tabs-panel-color | var(--color-foreground) | Controls panel text color. |
| --tabs-panel-font-size | var(--text-sm) | Controls panel text font size. |
| --tabs-panel-line-height | var(--line-height-text-sm) | Controls panel text line height. |
| --tabs-panel-focus-ring-offset | calc(var(--tabs-focus-ring-width, var(--border-width-sm)) * -1) | Controls panel focus ring offset. |
| --tabs-panel-padding | 1rem | Controls panel padding. |
| --tabs-radius | var(--radius-lg) | Controls the root border radius. |
| --tabs-tab-color | var(--color-muted-foreground) | Controls inactive tab text color. |
| --tabs-tab-color-active | var(--color-foreground) | Controls active tab text color. |
| --tabs-tab-color-hover | var(--color-foreground) | Controls hovered tab text color. |
| --tabs-tab-content-gap | 0.5rem | Controls spacing between tab icon and label. |
| --tabs-tab-disabled-opacity | var(--opacity-disabled) | Controls disabled tab opacity. |
| --tabs-tab-font-size | var(--text-sm) | Controls tab text font size. |
| --tabs-tab-font-weight | var(--weight-medium) | Controls tab text font weight. |
| --tabs-tab-height | 2rem | Controls each tab height. |
| --tabs-tab-icon-size | 1rem | Controls tab icon size. |
| --tabs-tab-icon-color | currentColor | Controls tab icon color. |
| --tabs-tab-line-height | var(--line-height-text-sm) | Controls tab text line height. |
| --tabs-tab-padding-x | 0.625rem | Controls each tab horizontal padding. |
| --tabs-tab-radius | var(--radius-sm) | Controls each tab border radius. |
| --tabs-tab-transition | var(--transition-default) | Controls tab text color transition. |
| --tabs-vertical-list-width | 12rem | Controls the list width in vertical orientation. |
| --tabs-vertical-min-height | 14rem | Controls the root min-height in vertical orientation. |
| --tabs-width | 32rem | Controls the root tabs width. |
Interactive variables scoped for docs preview without changing size scale tokens.
| Property | Value | Default | Description |
|---|---|---|---|
| --tabs-bg | var(--color-background) | Controls root background color. | |
| --tabs-border-color | var(--color-border) | Controls root border color. | |
| --tabs-border-width | var(--border-width-sm) | Controls root border width. | |
| --tabs-focus-ring-color | var(--color-ring) | Controls tab and panel focus ring color. | |
| --tabs-list-bg | var(--color-muted) | Controls tab list background color. | |
| --tabs-panel-color | var(--color-foreground) | Controls panel text color. | |
| --tabs-radius | var(--radius-lg) | Controls root border radius. | |
| --tabs-tab-color | var(--color-muted-foreground) | Controls inactive tab text color. | |
| --tabs-tab-color-active | var(--color-foreground) | Controls active tab text color. | |
| --tabs-tab-color-hover | var(--color-foreground) | Controls hovered tab text color. | |
| --tabs-indicator-bg | var(--color-background) | Controls indicator background. |
Anatomy
Tabs is composed from a root state machine, one tab list, tab buttons, and matching panels.
TabsList renders the moving indicator internally by default, so consumers do not need to place a
service Indicator part in the public composition.
Tabs
├─ TabsList
│ ├─ TabsTab[value]
│ │ └─ label or TabsTabContent
│ │ ├─ TabsTabIcon
│ │ └─ TabsTabLabel
│ └─ indicator
└─ TabsPanel[value]
└─ content<Tabs defaultValue="overview">
<TabsList>
<TabsTab value="overview">Overview</TabsTab>
<TabsTab value="projects">Projects</TabsTab>
</TabsList>
<TabsPanel value="overview">Overview content</TabsPanel>
<TabsPanel value="projects">Projects content</TabsPanel>
</Tabs>| Part | Role |
|---|---|
Tabs | Root state machine. Controls selected value, orientation, variant, and controlled/uncontrolled state. |
TabsList | Groups tab buttons and owns keyboard list behavior. It renders the active indicator internally. |
TabsTab | Interactive tab button. Its value must match one TabsPanel value. |
TabsTabContent | Optional wrapper for compound tab labels, usually icon plus text. |
TabsTabIcon | Optional icon wrapper. Use it with your application icons or the icons exported by moduix. |
TabsTabLabel | Optional text wrapper used with TabsTabContent. |
TabsPanel | Content region displayed when its matching tab is active. Use keepMounted when hidden DOM must remain. |
indicator | Internal service slot that follows the active tab. Style it through TabsList classNames.indicator. |
Composition
Use Tabs for state props such as defaultValue, value, onValueChange, orientation,
and variant. Base UI props pass through to the matching visible parts: TabsList
supports activateOnFocus and loopFocus, TabsTab supports disabled, render, and
nativeButton, and TabsPanel supports keepMounted.
Visible parts accept className. The internal indicator is not part of the required composition:
style it with TabsList classNames.indicator, disable it with withIndicator={false}, or pass
non-class Base UI indicator props through TabsList slotProps.indicator.
import { Tabs, TabsList, TabsPanel, TabsTab } from 'moduix';
import styles from './custom-indicator-tabs-demo.module.css';
export function CustomIndicatorTabsDemo() {
return (
<Tabs defaultValue="profile" className={styles.root}>
<TabsList
className={styles.list}
classNames={{ indicator: styles.indicator }}
slotProps={{ indicator: { renderBeforeHydration: true } }}
>
<TabsTab value="profile" className={styles.tab}>
Profile
</TabsTab>
<TabsTab value="security" className={styles.tab}>
Security
</TabsTab>
</TabsList>
<TabsPanel value="profile" className={styles.panel}>
Profile settings
</TabsPanel>
<TabsPanel value="security" className={styles.panel}>
Security settings
</TabsPanel>
</Tabs>
);
}Examples
Vertical
Use orientation="vertical" when the tab list should sit next to the active panel.
import { Tabs, TabsList, TabsPanel, TabsTab,} from "moduix";export function VerticalTabsDemo() { return ( <Tabs defaultValue="overview" orientation="vertical"> <TabsList> {items.map((item) => ( <TabsTab key={item.value} value={item.value}> {item.title} </TabsTab> ))} </TabsList> {items.map((item) => ( <TabsPanel key={item.value} value={item.value}> {item.content} </TabsPanel> ))} </Tabs> );}const items = [ { value: "overview", title: "Overview", content: "Review project status, team velocity, workloads and activity highlights in one place.", }, { value: "projects", title: "Projects", content: "Track active workstreams, owners and milestones across all departments.", }, { value: "account", title: "Account", content: "Manage personal settings, team settings, notifications and access preferences.", },];Activate On Focus
Pass activateOnFocus to TabsList when arrow-key focus should immediately select a tab.
import { Tabs, TabsList, TabsPanel, TabsTab,} from "moduix";export function ActivateOnFocusTabsDemo() { return ( <Tabs defaultValue="overview"> <TabsList activateOnFocus> {items.map((item) => ( <TabsTab key={item.value} value={item.value}> {item.title} </TabsTab> ))} </TabsList> {items.map((item) => ( <TabsPanel key={item.value} value={item.value}> {item.content} </TabsPanel> ))} </Tabs> );}const items = [ { value: "overview", title: "Overview", content: "Review project status, team velocity, workloads and activity highlights in one place.", }, { value: "projects", title: "Projects", content: "Track active workstreams, owners and milestones across all departments.", }, { value: "account", title: "Account", content: "Manage personal settings, team settings, notifications and access preferences.", },];Line
Use variant="line" for a compact indicator that sits along the active tab edge.
import { Tabs, TabsList, TabsPanel, TabsTab,} from "moduix";export function LineTabsDemo() { return ( <Tabs defaultValue="overview" variant="line"> <TabsList> {items.map((item) => ( <TabsTab key={item.value} value={item.value}> {item.title} </TabsTab> ))} </TabsList> {items.map((item) => ( <TabsPanel key={item.value} value={item.value}> {item.content} </TabsPanel> ))} </Tabs> );}const items = [ { value: "overview", title: "Overview", content: "Review project status, team velocity, workloads and activity highlights in one place.", }, { value: "projects", title: "Projects", content: "Track active workstreams, owners and milestones across all departments.", }, { value: "account", title: "Account", content: "Manage personal settings, team settings, notifications and access preferences.", },];Without Indicator
Set withIndicator={false} on TabsList when the active tab should be shown only through tab state styles.
import { Tabs, TabsList, TabsPanel, TabsTab,} from "moduix";export function WithoutIndicatorTabsDemo() { return ( <Tabs defaultValue="overview"> <TabsList withIndicator={false}> {items.map((item) => ( <TabsTab key={item.value} value={item.value}> {item.title} </TabsTab> ))} </TabsList> {items.map((item) => ( <TabsPanel key={item.value} value={item.value}> {item.content} </TabsPanel> ))} </Tabs> );}const items = [ { value: "overview", title: "Overview", content: "Review project status, team velocity, workloads and activity highlights in one place.", }, { value: "projects", title: "Projects", content: "Track active workstreams, owners and milestones across all departments.", }, { value: "account", title: "Account", content: "Manage personal settings, team settings, notifications and access preferences.", },];Controlled
Control the active tab from React state when the selected panel needs to coordinate with other UI.
import { Tabs, TabsList, TabsPanel, TabsTab, type TabsValue,} from "moduix";import { useState } from "react";export function ControlledTabsDemo() { const [value, setValue] = useState("projects" as TabsValue); return ( <Tabs value={value} onValueChange={setValue}> <TabsList> {items.map((item) => ( <TabsTab key={item.value} value={item.value}> {item.title} </TabsTab> ))} </TabsList> {items.map((item) => ( <TabsPanel key={item.value} value={item.value}> {item.content} </TabsPanel> ))} </Tabs> );}const items = [ { value: "overview", title: "Overview", content: "Review project status, team velocity, workloads and activity highlights in one place.", }, { value: "projects", title: "Projects", content: "Track active workstreams, owners and milestones across all departments.", }, { value: "account", title: "Account", content: "Manage personal settings, team settings, notifications and access preferences.", },];Links
Use render with nativeButton={false} when tabs should render as links.
import { Tabs, TabsList, TabsPanel, TabsTab,} from "moduix";export function LinkTabsDemo() { return ( <Tabs defaultValue="overview"> <TabsList> {items.map((item) => ( <TabsTab key={item.value} value={item.value} nativeButton={false} render={<a href={"#" + item.value} />} > {item.title} </TabsTab> ))} </TabsList> {items.map((item) => ( <TabsPanel key={item.value} value={item.value}> <span id={item.value}>{item.content}</span> </TabsPanel> ))} </Tabs> );}const items = [ { value: "overview", title: "Overview", content: "Review project status, team velocity, workloads and activity highlights in one place.", }, { value: "projects", title: "Projects", content: "Track active workstreams, owners and milestones across all departments.", }, { value: "account", title: "Account", content: "Manage personal settings, team settings, notifications and access preferences.", },];Custom Icons
Compose tab content with TabsTabContent, TabsTabIcon, and TabsTabLabel to use icons from your application or icon library.
import { HandshakeIcon, MapIcon, PresentIcon, Tabs, TabsList, TabsPanel, TabsTab, TabsTabContent, TabsTabIcon, TabsTabLabel,} from "moduix";export function IconTabsDemo() { return ( <Tabs defaultValue="overview"> <TabsList> <TabsTab value="overview"> <TabsTabContent> <TabsTabIcon> <HandshakeIcon /> </TabsTabIcon> <TabsTabLabel>Overview</TabsTabLabel> </TabsTabContent> </TabsTab> <TabsTab value="projects"> <TabsTabContent> <TabsTabIcon> <PresentIcon /> </TabsTabIcon> <TabsTabLabel>Projects</TabsTabLabel> </TabsTabContent> </TabsTab> <TabsTab value="account"> <TabsTabContent> <TabsTabIcon> <MapIcon /> </TabsTabIcon> <TabsTabLabel>Account</TabsTabLabel> </TabsTabContent> </TabsTab> </TabsList> {items.map((item) => ( <TabsPanel key={item.value} value={item.value}> {item.content} </TabsPanel> ))} </Tabs> );}const items = [ { value: "overview", content: "Review project status, team velocity, workloads and activity highlights in one place.", }, { value: "projects", content: "Track active workstreams, owners and milestones across all departments.", }, { value: "account", content: "Manage personal settings, team settings, notifications and access preferences.", },];Disabled Tab
Disable tabs that are visible but not available yet. Add keepMounted to panels when hidden panel
DOM must stay mounted for forms, measurements, or preserved local state.
import { Tabs, TabsList, TabsPanel, TabsTab,} from "moduix";export function DisabledTabTabsDemo() { return ( <Tabs defaultValue="overview"> <TabsList> <TabsTab value="overview">Overview</TabsTab> <TabsTab value="projects" disabled> Projects </TabsTab> <TabsTab value="account">Account</TabsTab> </TabsList> {items.map((item) => ( <TabsPanel key={item.value} value={item.value} keepMounted> {item.content} </TabsPanel> ))} </Tabs> );}const items = [ { value: "overview", title: "Overview", content: "Review project status, team velocity, workloads and activity highlights in one place.", }, { value: "projects", title: "Projects", content: "Track active workstreams, owners and milestones across all departments.", }, { value: "account", title: "Account", content: "Manage personal settings, team settings, notifications and access preferences.", },];Custom Styles
Use className, classNames.indicator, and CSS variables to reshape layout and visuals when the default Tabs style is too strong for the target composition.
import { Tabs, TabsList, TabsPanel, TabsTab,} from "moduix";import styles from "./inline-inputs-tabs-demo.module.css";export function InlineInputsTabsDemo() { return ( <Tabs defaultValue="name" className={styles.inlineRoot}> <TabsList className={styles.inlineList} classNames={{ indicator: styles.inlineIndicator }} slotProps={{ indicator: { renderBeforeHydration: true } }} > <TabsTab value="name" className={styles.inlineTab}> Name </TabsTab> <TabsTab value="email" className={styles.inlineTab}> Email </TabsTab> </TabsList> <TabsPanel value="name" className={styles.inlinePanel}> <input className={styles.inlineInput} placeholder="Full name" aria-label="Full name" /> </TabsPanel> <TabsPanel value="email" className={styles.inlinePanel}> <input className={styles.inlineInput} placeholder="Email" aria-label="Email" /> </TabsPanel> </Tabs> );}.inlineRoot { --tabs-bg: transparent; --tabs-border-color: transparent; --tabs-border-width: 0; --tabs-gap: var(--spacing-4); --tabs-list-bg: transparent; --tabs-list-border-width: 0; --tabs-list-padding: 0; --tabs-list-padding-x: 0; --tabs-list-padding-y: 0; display: flex; flex-direction: row; width: min(36rem, calc(100vw - 2rem)); align-items: center; justify-content: space-between; gap: var(--tabs-gap); color: var(--color-foreground);}.inlineList { position: relative; display: flex; align-items: center; gap: var(--spacing-1); flex-shrink: 0;}.inlineTab { height: var(--size-md); margin: 0; appearance: none; border: 0; border-radius: var(--radius-sm); padding-inline: var(--spacing-3); background: transparent; color: var(--color-muted-foreground); font: inherit; font-size: var(--text-sm); line-height: var(--line-height-text-sm); cursor: pointer; &[data-active] { color: var(--color-foreground); }}.inlineIndicator { position: absolute; top: auto; left: 0; bottom: 0; width: var(--active-tab-width); height: var(--border-width-md); border-radius: var(--radius-full); background: var(--color-foreground); translate: var(--active-tab-left) 0; transition: translate 200ms ease, width 200ms ease;}.inlinePanel { flex: 1; min-width: 0; &[hidden]:not([hidden='until-found']) { display: none; }}.inlineInput { box-sizing: border-box; width: 100%; height: var(--size-md); border: var(--border-width-sm) solid var(--color-border); border-radius: var(--radius-sm); padding-inline: var(--spacing-2); background: var(--color-background); color: var(--color-foreground); font: inherit; font-size: var(--text-sm);}