> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tabs

> Switch between related panels with accessible tab navigation.

export const StoryEmbed = ({storyId, title, height = 120}) => <>
    <Tabs>
      <Tab title="Light">
        <iframe title={`${title} (light)`} src={`https://storybook.twenty.com/iframe.html?id=${storyId}&viewMode=story&globals=colorScheme:light`} width="100%" height={height} loading="lazy" allow="clipboard-write" style={{
  border: 0
}} />
      </Tab>
      <Tab title="Dark">
        <iframe title={`${title} (dark)`} src={`https://storybook.twenty.com/iframe.html?id=${storyId}&viewMode=story&globals=colorScheme:dark`} width="100%" height={height} loading="lazy" allow="clipboard-write" style={{
  border: 0
}} />
      </Tab>
    </Tabs>
    <a href={`https://storybook.twenty.com/?path=/story/${storyId}`}>
      Open in Storybook
    </a>
  </>;

`Tabs` connects each tab to a related content panel using a shared `value`. Keep values unique within a root. Use [TabButton](/ui/components/navigation/tab-button) for route links and adjacent actions.

<StoryEmbed storyId="ui-navigation-tabs--display" title="Tabs example" height={160} />

## Usage

Use `defaultValue` to set the initial selection. The root owns subsequent changes.

```tsx theme={null}
import { Tabs } from 'twenty-ui/primitives/navigation';

export const RecordTabs = () => (
  <Tabs.Root defaultValue="overview">
    <Tabs.List aria-label="Record details">
      <Tabs.Tab value="overview">Overview</Tabs.Tab>
      <Tabs.Tab value="activity">Activity</Tabs.Tab>
      <Tabs.Tab value="settings">Settings</Tabs.Tab>
      <Tabs.Indicator />
    </Tabs.List>
    <Tabs.Panel value="overview">Record overview</Tabs.Panel>
    <Tabs.Panel value="activity">Recent activity</Tabs.Panel>
    <Tabs.Panel value="settings">Record settings</Tabs.Panel>
  </Tabs.Root>
);
```

`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 inside `Tabs.List`. Place their panels alongside the list, all within the same `Tabs.Root`.

```text theme={null}
Tabs.Root
├── Tabs.List
│   ├── Tabs.Tab
│   ├── Tabs.Tab
│   └── Tabs.Indicator
├── Tabs.Panel
└── Tabs.Panel
```

| Part | Requirement | Purpose |
| - | - | - |
| `Root` | Required | Owns the selected value and orientation. |
| `List` | Required | Groups tabs and supplies keyboard focus navigation. |
| `Tab` | One per panel | Activates the panel with the same `value`. |
| `Panel` | One per content view | Displays its content when its `value` is active. |
| `Indicator` | Optional | Shows the position of the active tab with themed styling. |

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

Pass `value` 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()`.

```tsx theme={null}
import { useState } from 'react';
import { Tabs } from 'twenty-ui/primitives/navigation';

export const RecordTabs = () => {
  const [tab, setTab] = useState('overview');

  return (
    <Tabs.Root value={tab} onValueChange={setTab}>
      <Tabs.List aria-label="Record details">
        <Tabs.Tab value="overview">Overview</Tabs.Tab>
        <Tabs.Tab value="activity" badge={3}>
          Activity
        </Tabs.Tab>
        <Tabs.Indicator />
      </Tabs.List>
      <Tabs.Panel value="overview">Company and contact details.</Tabs.Panel>
      <Tabs.Panel value="activity">Recent calls and emails.</Tabs.Panel>
    </Tabs.Root>
  );
};
```

User activation has reason `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:

```tsx theme={null}
import { Tabs } from 'twenty-ui/primitives/navigation';

export const ProtectedTabs = () => (
  <Tabs.Root
    defaultValue="overview"
    onValueChange={(value, details) => {
      if (value === 'restricted') {
        details.cancel();
      }
    }}
  >
    <Tabs.List aria-label="Account sections">
      <Tabs.Tab value="overview">Overview</Tabs.Tab>
      <Tabs.Tab value="restricted">Restricted</Tabs.Tab>
      <Tabs.Indicator />
    </Tabs.List>
    <Tabs.Panel value="overview">Account details.</Tabs.Panel>
    <Tabs.Panel value="restricted">Restricted content.</Tabs.Panel>
  </Tabs.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.

```tsx theme={null}
import { Tabs } from 'twenty-ui/primitives/navigation';

export const SettingsTabs = () => (
  <Tabs.Root defaultValue="profile" orientation="vertical">
    <Tabs.List aria-label="Settings" activateOnFocus loopFocus={false}>
      <Tabs.Tab value="profile">Profile</Tabs.Tab>
      <Tabs.Tab value="billing" disabled>
        Billing
      </Tabs.Tab>
      <Tabs.Tab value="notifications">Notifications</Tabs.Tab>
      <Tabs.Indicator />
    </Tabs.List>
    <Tabs.Panel value="profile">Profile settings.</Tabs.Panel>
    <Tabs.Panel value="billing">Billing settings.</Tabs.Panel>
    <Tabs.Panel value="notifications">Notification settings.</Tabs.Panel>
  </Tabs.Root>
);
```

Horizontal lists use Left and Right; vertical lists use Up and Down. Home and End focus the first and last enabled tabs. Disabled tabs are skipped. `loopFocus` defaults to `true`; set it to `false` to stop at either boundary.

Use [DirectionProvider](/ui/primitives/layout/direction-provider) 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

Use `keepMounted` 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 UI `render` 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.

```tsx theme={null}
import { useRef } from 'react';
import { Tabs } from 'twenty-ui/primitives/navigation';

export const ComposedTabs = () => {
  const panelRef = useRef<HTMLElement>(null);

  return (
    <Tabs.Root defaultValue="overview" render={<section />}>
      <Tabs.List aria-label="Account details">
        <Tabs.Tab value="overview" nativeButton render={<button />}>
          Overview
        </Tabs.Tab>
        <Tabs.Indicator render={<span data-testid="active-indicator" />} />
      </Tabs.List>
      <Tabs.Panel
        value="overview"
        render={<article />}
        ref={(element) => {
          panelRef.current = element;
        }}
      >
        Account details.
      </Tabs.Panel>
    </Tabs.Root>
  );
};
```

Keep `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

Use `highlighted` 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 from `twenty-ui/primitives/navigation`.

### Tabs.Root

<ParamField body="Root.className" type="string | ((state: TabsRootState) => string | undefined)">
  CSS class applied to the element, or a function that
  returns a class based on the component's state.
</ParamField>

<ParamField body="Root.defaultValue" type="any" default="0">
  The default value. Use when the component is not controlled.
  When the value is `null`, no Tab will be active.
</ParamField>

<ParamField body="Root.onValueChange" type="((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.
</ParamField>

<ParamField body="Root.orientation" type="&#x22;horizontal&#x22; | &#x22;vertical&#x22;" default="horizontal">
  The component orientation (layout flow direction).
</ParamField>

<ParamField body="Root.render" type="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.
</ParamField>

<ParamField body="Root.style" type="CSSProperties | ((state: TabsRootState) => CSSProperties | undefined)">
  Style applied to the element, or a function that
  returns a style object based on the component's state.
</ParamField>

<ParamField body="Root.value" type="any">
  The value of the currently active `Tab`. Use when the component is controlled.
  When the value is `null`, no Tab will be active.
</ParamField>

### Tabs.List

<ParamField body="List.activateOnFocus" type="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.
</ParamField>

<ParamField body="List.className" type="string | ((state: TabsListState) => string | undefined)">
  CSS class applied to the element, or a function that
  returns a class based on the component's state.
</ParamField>

<ParamField body="List.loopFocus" type="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.
</ParamField>

<ParamField body="List.render" type="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.
</ParamField>

<ParamField body="List.style" type="CSSProperties | ((state: TabsListState) => CSSProperties | undefined)">
  Style applied to the element, or a function that
  returns a style object based on the component's state.
</ParamField>

### Tabs.Tab

<ParamField body="Tab.badge" type="ReactNode">
  Content rendered after the label, such as a count.
</ParamField>

<ParamField body="Tab.className" type="string | ((state: TabsTabState) => string | undefined)">
  CSS class applied to the element, or a function that
  returns a class based on the component's state.
</ParamField>

<ParamField body="Tab.disabled" type="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.
</ParamField>

<ParamField body="Tab.endIcon" type="ReactNode">
  Decorative content after the label and before the badge.
</ParamField>

<ParamField body="Tab.highlighted" type="boolean" default="false">
  Emphasizes the tab content without changing selection.
</ParamField>

<ParamField body="Tab.nativeButton" type="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>`).
</ParamField>

<ParamField body="Tab.render" type="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.
</ParamField>

<ParamField body="Tab.size" type="&#x22;md&#x22; | &#x22;sm&#x22;" default="sm">
  Visual size of the tab.
</ParamField>

<ParamField body="Tab.startIcon" type="ReactNode">
  Icon rendered before the label.
</ParamField>

<ParamField body="Tab.style" type="CSSProperties | ((state: TabsTabState) => CSSProperties | undefined)">
  Style applied to the element, or a function that
  returns a style object based on the component's state.
</ParamField>

<ParamField body="Tab.value" type="any" required>
  The value of the Tab.
</ParamField>

### Tabs.Indicator

<ParamField body="Indicator.className" type="string | ((state: TabsIndicatorState) => string | undefined)">
  CSS class applied to the element, or a function that
  returns a class based on the component's state.
</ParamField>

<ParamField body="Indicator.render" type="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.
</ParamField>

<ParamField body="Indicator.renderBeforeHydration" type="boolean" default="false">
  Whether to render itself before React hydrates.
  This minimizes the time that the indicator isn't visible after server-side rendering.
</ParamField>

<ParamField body="Indicator.style" type="CSSProperties | ((state: TabsIndicatorState) => CSSProperties | undefined)">
  Style applied to the element, or a function that
  returns a style object based on the component's state.
</ParamField>

### Tabs.Panel

<ParamField body="Panel.className" type="string | ((state: TabsPanelState) => string | undefined)">
  CSS class applied to the element, or a function that
  returns a class based on the component's state.
</ParamField>

<ParamField body="Panel.keepMounted" type="boolean" default="false">
  Whether to keep the HTML element in the DOM while the panel is hidden.
</ParamField>

<ParamField body="Panel.render" type="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.
</ParamField>

<ParamField body="Panel.style" type="CSSProperties | ((state: TabsPanelState) => CSSProperties | undefined)">
  Style applied to the element, or a function that
  returns a style object based on the component's state.
</ParamField>

<ParamField body="Panel.value" type="any" required>
  The value of the TabPanel. It will be shown when the Tab with the corresponding value is active.
</ParamField>

## Migration

Add an explicit `Tabs.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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.