# Tabs

> Tabs switch between related views in the same place.

Source: /docs/components/tabs

```tsx
import { Tabs } from '@aksornproject/aksara-ui'
```

## Basic tabs

`Tabs.Root` holds the selection. `Tabs.List` is the tab row and needs an `aria-label` or `aria-labelledby`. Each `Tabs.Tab` and `Tabs.Panel` pair shares a `value`, a string or a number. `defaultValue` picks the first selected tab.

```tsx
import { Tabs } from '@aksornproject/aksara-ui'

export default function TabsBasic() {
  return (
    <Tabs.Root defaultValue="overview" className="w-full">
      <Tabs.List aria-label="Course views">
        <Tabs.Tab value="overview">Overview</Tabs.Tab>
        <Tabs.Tab value="lessons">Lessons</Tabs.Tab>
        <Tabs.Tab value="grades">Grades</Tabs.Tab>
      </Tabs.List>
      <Tabs.Panel value="overview">
        Algebra for upper secondary students, taught over 12 weeks.
      </Tabs.Panel>
      <Tabs.Panel value="lessons">
        24 lessons, from linear equations to quadratic functions.
      </Tabs.Panel>
      <Tabs.Panel value="grades">
        Grades appear here after the first assignment.
      </Tabs.Panel>
    </Tabs.Root>
  )
}
```

## Sizes

`size` on the Root sets the tab height for every tab. `small` is 40px and the default, `medium` is 44px and `large` is 48px.

```tsx
import { Tabs, type TabsSize } from '@aksornproject/aksara-ui'

const sizes: TabsSize[] = ['small', 'medium', 'large']

export default function TabsSizes() {
  return (
    <div className="flex w-full flex-col gap-spacing-16">
      {sizes.map((size) => (
        <Tabs.Root key={size} size={size} defaultValue="overview">
          <Tabs.List aria-label={`Course views, ${size}`}>
            <Tabs.Tab value="overview">Overview</Tabs.Tab>
            <Tabs.Tab value="lessons">Lessons</Tabs.Tab>
            <Tabs.Tab value="grades">Grades</Tabs.Tab>
          </Tabs.List>
        </Tabs.Root>
      ))}
    </div>
  )
}
```

## Slots and disabled tabs

`leading` sits before the label. `selectedContent` shows only while the tab is selected and always reserves its box, so selection never moves the row. Both are decorative. `endContent` holds compact noninteractive content, such as a count, and its text is part of the tab name.

`disabled` keeps a tab focusable but it never activates.

```tsx
import {
  IconInterfaceEssentialDoneCheck,
  IconInterfaceEssentialSearch,
} from '@aksornproject/aksara-icons'
import { Tabs } from '@aksornproject/aksara-ui'

export default function TabsSlots() {
  return (
    <Tabs.Root defaultValue="saved" className="w-full">
      <Tabs.List aria-label="Library views">
        <Tabs.Tab value="search" leading={<IconInterfaceEssentialSearch />}>
          Search
        </Tabs.Tab>
        <Tabs.Tab
          value="saved"
          selectedContent={<IconInterfaceEssentialDoneCheck />}
        >
          Saved
        </Tabs.Tab>
        <Tabs.Tab value="assigned" endContent={<span>3</span>}>
          Assigned
        </Tabs.Tab>
        <Tabs.Tab value="archived" disabled>
          Archived
        </Tabs.Tab>
      </Tabs.List>
      <Tabs.Panel value="search">
        Search the library by title or topic.
      </Tabs.Panel>
      <Tabs.Panel value="saved">Worksheets you saved for later.</Tabs.Panel>
      <Tabs.Panel value="assigned">
        Three worksheets are due this week.
      </Tabs.Panel>
    </Tabs.Root>
  )
}
```

## Menu tabs

`Tabs.Menu` is a tab that opens a menu of options. Pass Menu content to `menu`. It has its own `value` and panel like a regular tab. Click, Enter and Space select it and open the popup. Arrow keys open the popup without selecting.

Selection and the open state stay independent. `open`, `defaultOpen` and `onOpenChange` control the popup like `Menu.Root`.

```tsx
import { Menu, Tabs } from '@aksornproject/aksara-ui'

export default function TabsMenu() {
  return (
    <Tabs.Root defaultValue="details" className="w-full">
      <Tabs.List aria-label="Course views">
        <Tabs.Tab value="details">Details</Tabs.Tab>
        <Tabs.Tab value="units">Units</Tabs.Tab>
        <Tabs.Menu
          value="versions"
          menu={
            <>
              <Menu.Item>Published</Menu.Item>
              <Menu.Item>Drafts</Menu.Item>
            </>
          }
        >
          Versions
        </Tabs.Menu>
      </Tabs.List>
      <Tabs.Panel value="details">
        Course title, subject and grade level.
      </Tabs.Panel>
      <Tabs.Panel value="units">Five units, in teaching order.</Tabs.Panel>
      <Tabs.Panel value="versions" keepMounted>
        Pick a version from the menu.
      </Tabs.Panel>
    </Tabs.Root>
  )
}
```

## Fill layout

`layout="fill"` on the List shares the row equally, with a 96px minimum per tab. Long labels truncate, and the list scrolls when the tabs do not fit. The default `hug` layout sizes each tab to its label.

```tsx
import { Tabs } from '@aksornproject/aksara-ui'

export default function TabsFill() {
  return (
    <Tabs.Root defaultValue="students" className="w-full max-w-[480px]">
      <Tabs.List aria-label="Class views" layout="fill">
        <Tabs.Tab value="students">Students</Tabs.Tab>
        <Tabs.Tab value="assignments">Assignments</Tabs.Tab>
        <Tabs.Tab value="attendance">Attendance</Tabs.Tab>
      </Tabs.List>
    </Tabs.Root>
  )
}
```

## Actions and overflow

`actions` holds your buttons after a vertical separator. `overflow` holds a button or menu trigger at the end of the row. Both sit outside the tablist, so they are not tabs. `divider={false}` removes the rule under the row.

```tsx
import { IconInterfaceEssentialDots } from '@aksornproject/aksara-icons'
import { Button, Menu, Tabs } from '@aksornproject/aksara-ui'

export default function TabsActions() {
  return (
    <Tabs.Root defaultValue="students" className="w-full">
      <Tabs.List
        aria-label="Class views"
        actions={
          <Button size="xs" variant="tertiary-accent">
            Export
          </Button>
        }
        overflow={
          <Menu.Root>
            <Menu.Trigger
              aria-label="More actions"
              render={<button type="button" />}
            >
              <IconInterfaceEssentialDots />
            </Menu.Trigger>
            <Menu.Portal>
              <Menu.Positioner align="end">
                <Menu.Popup>
                  <Menu.Item>Print</Menu.Item>
                  <Menu.Item>Archive class</Menu.Item>
                </Menu.Popup>
              </Menu.Positioner>
            </Menu.Portal>
          </Menu.Root>
        }
      >
        <Tabs.Tab value="students">Students</Tabs.Tab>
        <Tabs.Tab value="assignments">Assignments</Tabs.Tab>
      </Tabs.List>
      <Tabs.Panel value="students">28 students enrolled.</Tabs.Panel>
      <Tabs.Panel value="assignments">4 assignments this term.</Tabs.Panel>
    </Tabs.Root>
  )
}
```

## Controlled tabs

Pass `value` and `onValueChange` to control the selection. `onValueChange` reports the next value as `string | number | null` with the Base UI event details. `value` and `defaultValue` cannot be combined, and `null` selects nothing.

```tsx
import { Tabs, type TabsValue } from '@aksornproject/aksara-ui'
import { useState } from 'react'

export default function TabsControlled() {
  const [value, setValue] = useState<TabsValue | null>('week')

  return (
    <div className="flex w-full flex-col gap-spacing-8">
      <Tabs.Root value={value} onValueChange={setValue}>
        <Tabs.List aria-label="Schedule range">
          <Tabs.Tab value="day">Day</Tabs.Tab>
          <Tabs.Tab value="week">Week</Tabs.Tab>
          <Tabs.Tab value="month">Month</Tabs.Tab>
        </Tabs.List>
      </Tabs.Root>
      <p>Showing the {value} view.</p>
    </div>
  )
}
```

## Accessibility

- Arrow keys move focus without selecting. Enter and Space select. Home and End jump to the ends, and focus loops by default. `activateOnFocus` and `loopFocus` on the List change this.
- Base UI supplies the roving tabindex and links each tab to its panel.
- The List requires an accessible name. TypeScript rejects a List without `aria-label` or `aria-labelledby`.
- Menu tabs expose `aria-haspopup="menu"` and `aria-expanded`. Escape closes the popup and returns focus to the tab.
- `leading`, `selectedContent`, the chevron, the indicator and the divider are `aria-hidden`. `endContent` stays in the accessible name.
- Only horizontal tabs exist. `orientation` accepts `horizontal` alone.

## API

### Tabs.Root

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `style` | `React.CSSProperties \| ((state: TabsRootState) => React.CSSProperties \| undefined)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `value` | `TabsValue \| null` |  | The selected tab value, for a controlled Root. `null` selects nothing. |
| `defaultValue` | `TabsValue \| null` |  | Not accepted with `value`. The tab selected on first render, for an uncontrolled Root. `null` selects nothing. |
| `size` | `"large" \| "medium" \| "small"` | `'small'` | Sets the 40, 44 or 48px tab height for every tab. |
| `onValueChange` | `TabsValueChange` |  | Called with the next value, narrowed to `string \| number \| null`, and the Base UI event details. |
| `orientation` | `"horizontal"` | `'horizontal'` | Only horizontal. Tabs has no vertical layout. |
| `className` | `string` |  | Classes merged last with `cn()`. |

### Tabs.List

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `style` | `React.CSSProperties \| ((state: TabsListState) => React.CSSProperties \| undefined)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `activateOnFocus` | `boolean` | `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. |
| `loopFocus` | `boolean` | `true` | Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. |
| `aria-label` | `string` |  | Names the tablist. Pass this or `aria-labelledby`. |
| `aria-labelledby` | `string` |  | The ID of the element that names the tablist. Pass this or `aria-label`. |
| `layout` | `"fill" \| "hug"` | `'hug'` | `hug` sizes each tab to its label. `fill` shares the row equally with a 96px floor and truncates labels. |
| `divider` | `boolean` | `true` | Draws the 1px rule under the row. |
| `actions` | `ReactNode` |  | Your buttons after a vertical separator, outside the tablist. |
| `overflow` | `ReactNode` |  | Your button or menu trigger at the end of the row, outside the tablist. |
| `variant` | `"underline"` |  |  |
| `className` | `string` |  | Classes merged last onto the `role="tablist"` element, not the frame. |

### Tabs.Tab

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `style` | `React.CSSProperties \| ((state: TabsTabState) => React.CSSProperties \| undefined)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `disabled` | `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. |
| `value` (required) | `TabsValue` |  | The tab identity, linked to the `Tabs.Panel` with the same value. |
| `children` (required) | `ReactNode` |  | The label. Text or text formatting only. |
| `leading` | `ReactNode` |  | Content before the label, such as an icon. Decorative and `aria-hidden`. |
| `className` | `string` |  | Classes merged last with `cn()`. |
| `endContent` | `ReactNode` |  | Content after the label, such as a count. Noninteractive. Its text is part of the tab name. |
| `trailingNode` | `ReactNode` |  |  |
| `selectedContent` | `ReactNode` |  | Decorative content shown while the tab is selected. Its box is always reserved, so the tab does not change width. |
| `size` | `"large" \| "medium" \| "small"` |  | Overrides the Root `size` for this tab. Prefer setting `size` on Root so every tab matches. |
| `widthFull` | `boolean` | `false` |  |

### Tabs.Menu

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `style` | `React.CSSProperties \| ((state: TabsTabState) => React.CSSProperties \| undefined)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `disabled` | `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. |
| `value` (required) | `TabsValue` |  | The tab identity, linked to the `Tabs.Panel` with the same value. |
| `children` (required) | `ReactNode` |  | The label. Text or text formatting only. |
| `leading` | `ReactNode` |  | Content before the label, such as an icon. Decorative and `aria-hidden`. |
| `className` | `string` |  | Classes merged last with `cn()`. |
| `defaultOpen` | `boolean` | `false` | Whether the menu is initially open. To render a controlled menu, use the `open` prop instead. |
| `onOpenChange` | `((open: boolean, eventDetails: MenuRoot.ChangeEventDetails) => void)` |  | Event handler called when the menu is opened or closed. |
| `open` | `boolean` |  | Whether the menu is currently open. |
| `menu` (required) | `ReactNode` |  | `Menu.Item`, `Menu.Group` and other Menu content for the popup. |

### Tabs.Panel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `style` | `React.CSSProperties \| ((state: TabsPanelState) => React.CSSProperties \| undefined)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `keepMounted` | `boolean` | `false` | Whether to keep the HTML element in the DOM while the panel is hidden. |
| `value` (required) | `TabsValue` |  | Shows this panel while the tab with the same value is selected. |
| `className` | `string` |  | Classes merged last, so `p-0` removes the default 16px padding. |