# Tabs

> A row of tabs for the sections of one page, each with its own content; the selected tab is a yellow sticker, and the row scrolls when the tabs don't fit.

Source: https://raft-design.botiverse.build/components/tabs. Generated from the current release; where it differs from what you remember, this page is right.

Tabs switch between the sections of one page, each with its own content: a channel's chat, tasks and files, for example. The selected tab is a yellow sticker, the same "you are here" as a sidebar row or a segmented control. When the tabs don't fit, the row scrolls sideways.

```tsx
import { Tabs, TabsList, TabsPanel, TabsTab } from "@botiverse/raft-ui";
import type { TabsPlacement } from "@botiverse/raft-ui";

function ChannelSettingsTabs({ placement }: { placement: TabsPlacement }) {
  return (
    <Tabs defaultValue="general" placement={placement} className="gap-3">
      <TabsList variant="underline">
        <TabsTab value="general">General</TabsTab>
        <TabsTab value="members">Members</TabsTab>
        <TabsTab value="notifications">Notifications</TabsTab>
      </TabsList>
      <TabsPanel value="general" className="p-3 text-xs text-foreground-muted">
        Name, topic, and description of #design-review.
      </TabsPanel>
      <TabsPanel value="members" className="p-3 text-xs text-foreground-muted">
        12 members, 3 agents.
      </TabsPanel>
      <TabsPanel value="notifications" className="p-3 text-xs text-foreground-muted">
        Notify me about mentions only.
      </TabsPanel>
    </Tabs>
  );
}

export function TabsPlacements() {
  return (
    <>
      <ChannelSettingsTabs placement="top" />
      <ChannelSettingsTabs placement="bottom" />
      <ChannelSettingsTabs placement="left" />
      <ChannelSettingsTabs placement="right" />
    </>
  );
}
```

## When to use

| Situation                                       | Use                                                                          |
| ----------------------------------------------- | ---------------------------------------------------------------------------- |
| Sections of one page, each with its own content | Tabs                                                                         |
| Another view of the same content                | [SegmentedControl](https://raft-design.botiverse.build/components/segmented-control.md)                |
| The steps of a flow, in order                   | [Steps](https://raft-design.botiverse.build/components/steps.md)                                                   |
| A product's destinations                        | [AppRail](https://raft-design.botiverse.build/components/app-rail.md), [MobileNav](https://raft-design.botiverse.build/components/mobile-nav.md) |

A place with several kinds of content, such as a channel, shows them as one row of sticker tabs under its header ([Navigation](https://raft-design.botiverse.build/patterns/navigation.md#several-views-of-one-place)).

## Anatomy

- `Tabs`: the root. It holds the selected value and the `placement`.
- `TabsList`: the row of tabs.
- `TabsTab`: one tab, with an optional icon before a `TabsLabel`.
- `TabsPanel`: the content of one tab.
- `SortableTabsList` and `SortableTabsTab`: a row whose tabs people can reorder by dragging. `useOrderedTabs` restores a saved order.

## Variants

| Variant               | Use for                                                                                                                          |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `variant="default"`   | The sticker tab: the selected tab is a yellow sticker. The default                                                               |
| `variant="underline"` | Many tabs, or a place that already has a lot of color: the selected tab is ink text with a 2px ink line on the row's bottom edge |

The sticker tab is the default, and the one the product ships: a selected tab says "you are here", like a selected sidebar row or segment, so it takes the same yellow sticker ([Navigation](https://raft-design.botiverse.build/patterns/navigation.md#where-you-are)). The underline stays a variant. `placement` puts the row on the top (the default), bottom, left or right of the panels.

## Behavior

- When the tabs don't fit, the row scrolls on its own. Tabs never wrap onto a second line and never shrink their text.
- The row shows no scrollbar, and a vertical mouse wheel scrolls it sideways.
- The side that hides tabs fades out over 32px.
- The selected tab always scrolls into view.

## A tab's own controls

When a tab's content has controls of its own (a view switch, filters, search, a sort), they go in one toolbar row at the top of the tab's content, under the tabs. Every tab with controls uses the same row, the one the Tasks tab draws ([TasksPanelToolbar](https://raft-design.botiverse.build/components/tasks.md)), so a person who has learned one tab knows where to look in the next:

- The row has its own height and padding, the content margin at its sides and a `line-seam` below it, and it stays pinned while the content scrolls under it. Content never starts flush under the row, and never scrolls up against the tabs.
- Filters and search start at the row's left; a [SegmentedControl](https://raft-design.botiverse.build/components/segmented-control.md) that switches the view goes at its right end. A view switch on the left would sit right under the selected tab, and two yellow stickers stacked one above the other read as the same level.
- A tab with no controls has no row.

## Content

- Label tabs with 1 or 2 words in Title Case: Chat, Tasks, Files ([Typography](https://raft-design.botiverse.build/foundations/typography.md#casing)).
- Put a count in its own Space Mono element after the label, never in the label's text.

## Accessibility

- `Tabs` wraps the Base UI Tabs, which provides the tab list, tab and panel semantics and the arrow keys.
- Name the `TabsList` after the place it belongs to: `aria-label="launch-week"`.
- Give every icon-only tab an `aria-label`.
- Sortable tabs are reordered with a pointer or by touch; there is no keyboard reordering.

## Rules

- Use one row of tabs per screen.
- Make a sticker tab 32px high, with a 44px hit area and 8px of row above and below it, so the selected sticker sits as far from the line above as from the line below. Its width follows its label, and it has no track behind it.
- Make an underline tab 44px high, 20px from the next, with text only.
- Draw a `line-seam` under the row in both variants. When you change the size of the text or the row, check that the selected tab's space above and below is still equal.
- Start the first sticker on the content's edge, the same margin as the panel header's first element ([Page structure](https://raft-design.botiverse.build/patterns/page-structure.md#one-edge-per-column)). In dark mode the selected sticker has the die-cut edge like every sticker, so it lines up with the avatar in the header ([Dark mode](https://raft-design.botiverse.build/foundations/dark-mode.md#the-die-cut-edge)).

## API

### SortableTabsList

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onDragStart` | `((event: BaseUIEvent<DragEvent<HTMLDivElement>>) => void) & ((value: T) => void)` |  | A tab was picked up. On a touch screen the long press that armed it may have opened a menu the caller owns: close it here (DS › States › Dragging on a touch screen). |
| `onReorder` (required) | `(value: T[]) => void` |  |  |
| `value` (required) | `readonly T[]` |  |  |
| `variant` | `TabsListVariant` | `"default"` |  |
| `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. |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### SortableTabsTab

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `any` |  | The value of the Tab. |
| `disabled` | `boolean` |  | Whether the Tab is disabled. |
| `nativeButton` | `boolean` | `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>`). |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### Tabs

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultValue` | `T` |  |  |
| `onValueChange` | `(value: T, eventDetails: BaseTabs.Root.ChangeEventDetails) => void` |  |  |
| `placement` | `TabsPlacement` | `"top"` | Side of the panels the tab list sits on. `left` and `right` also make the tabs vertical (arrow up / down) unless `orientation` is set. |
| `value` | `T` |  |  |
| `variant` | `"default" \| "underline"` | `"default"` |  |
| `orientation` | `Orientation` | `'horizontal'` | The component orientation (layout flow direction). |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TabsLabel

Also takes the attributes of its element.

### TabsList

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `TabsListVariant` | `"default"` |  |
| `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. |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TabsPanel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `keepMounted` | `boolean` | `false` | Whether to keep the HTML element in the DOM while the panel is hidden. |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |
| `value` (required) | `any` |  | The value of the TabPanel. It will be shown when the Tab with the corresponding value is active. |

Also takes the attributes of its element.

### TabsTab

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `any` |  | The value of the Tab. |
| `disabled` | `boolean` |  | Whether the Tab is disabled. |
| `nativeButton` | `boolean` | `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>`). |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

## Changes

- 0.5.36 (Changed): Elegant `Tabs` underline: the bar starts on the 20px line at every width (the first icon lands at 32), and the strip has one source now — the indicator defaults to `bg-primary-400` and the placement blocks no longer draw a static hairline, so the conversation panel, the agent-details strip and the docs read the same look. Brutal underline: the conversation panel drops its asymmetric first-tab `pl-[22px]`; the strip sits at 16 and reads centred, as the agent-details strip.
