# SegmentedControl

> 2 to 4 segments in a gray track that switch how the same content is shown, in place; the selected segment is a yellow sticker.

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

A segmented control switches between 2 to 4 views of the same content, in place: a board or a list of the same tasks, a light or a dark appearance. The segments sit in a gray track, and the selected one is a yellow sticker.

```tsx
import { SegmentedControl, SegmentedControlItem, SegmentedControlLabel } from "@botiverse/raft-ui";
import { List, Columns3 } from "lucide-react";

type SegmentedControlViewModeCompositionProps = {
  value?: string;
  onValueChange?: (value: string) => void;
};

export function SegmentedControlViewModeComposition({
  value = "board",
  onValueChange = () => {},
}: SegmentedControlViewModeCompositionProps) {
  return (
    <SegmentedControl aria-label="Task view" value={value} onValueChange={onValueChange}>
      <SegmentedControlItem value="board">
        <Columns3 />
        <SegmentedControlLabel>Board</SegmentedControlLabel>
      </SegmentedControlItem>
      <SegmentedControlItem value="list">
        <List />
        <SegmentedControlLabel>List</SegmentedControlLabel>
      </SegmentedControlItem>
    </SegmentedControl>
  );
}
```

## When to use

| Situation                                                             | Use                                         |
| --------------------------------------------------------------------- | ------------------------------------------- |
| Another view of the same content: Board / List, Light / Dark / System | SegmentedControl                            |
| Sections of one page, each with its own content                       | [Tabs](https://raft-design.botiverse.build/components/tabs.md)                     |
| An answer submitted with a form                                       | [RadioGroup](https://raft-design.botiverse.build/components/radio-group.md) |
| More options than fit as segments                                     | [Select](https://raft-design.botiverse.build/components/select.md)               |

The full table is in [Choosing a component](https://raft-design.botiverse.build/components.md#picking-something). Where Board / List goes on a task list is in [Tasks](https://raft-design.botiverse.build/patterns/tasks.md).

## Anatomy

- `SegmentedControl`: the track, in `fill-muted`.
- `SegmentedControlItem`: one 32px segment, with an optional icon before its label.
- `SegmentedControlLabel`: the segment's text. It truncates.
- `SegmentedControlCount`: a count after the label, in Space Mono.

```tsx
import {
  SegmentedControl,
  SegmentedControlCount,
  SegmentedControlItem,
  SegmentedControlLabel,
} from "@botiverse/raft-ui";
import { AtSign, Inbox } from "lucide-react";

type SegmentedControlFiltersCompositionProps = {
  value?: string;
  onValueChange?: (value: string) => void;
};

export function SegmentedControlFiltersComposition({
  value = "all",
  onValueChange = () => {},
}: SegmentedControlFiltersCompositionProps) {
  return (
    <SegmentedControl aria-label="Filter messages" value={value} onValueChange={onValueChange}>
      <SegmentedControlItem value="all">
        <Inbox />
        <SegmentedControlLabel>All</SegmentedControlLabel>
        <SegmentedControlCount>12</SegmentedControlCount>
      </SegmentedControlItem>
      <SegmentedControlItem value="unread">
        <SegmentedControlLabel>Unread</SegmentedControlLabel>
        <SegmentedControlCount>3</SegmentedControlCount>
      </SegmentedControlItem>
      <SegmentedControlItem value="mentions">
        <AtSign />
        <SegmentedControlLabel>Mentions</SegmentedControlLabel>
      </SegmentedControlItem>
    </SegmentedControl>
  );
}
```

## States

- Selected: a yellow sticker with a bold label. It doesn't change on hover, like a selected tab; a press lays `sticker-medium` ([Hover and press](https://raft-design.botiverse.build/foundations/states.md#hover-and-press)).
- The other segments are `foreground-muted`, with the medium tier, `fill-medium`, on hover and press.
- Disabled: set on the whole control, as in [States](https://raft-design.botiverse.build/foundations/states.md).

```tsx
import { SegmentedControl, SegmentedControlItem, SegmentedControlLabel } from "@botiverse/raft-ui";
import { Languages } from "lucide-react";

function noop() {}

export function SegmentedControlDisabledComposition() {
  return (
    <SegmentedControl
      disabled
      aria-label="Translation mode"
      value="translated"
      onValueChange={noop}
    >
      <SegmentedControlItem value="translated">
        <Languages />
        <SegmentedControlLabel>Translated</SegmentedControlLabel>
      </SegmentedControlItem>
      <SegmentedControlItem value="original">
        <SegmentedControlLabel>Original</SegmentedControlLabel>
      </SegmentedControlItem>
    </SegmentedControl>
  );
}
```

## Behavior

- The segments sit 3px inside the track and 3px apart, so the gap between two of them is the gap to the track ([Spacing](https://raft-design.botiverse.build/foundations/spacing.md#spacing)).
- The track hugs its segments. Don't stretch it to fill its container: a stretched track leaves an empty run of gray beside the segments.
- The control never shrinks below its segments. When the row is too narrow, its neighbors give way first, then the segments show only their icons ([Responsive layout](https://raft-design.botiverse.build/patterns/responsive.md#when-a-row-runs-out-of-room)).
- To let the segments show only their icons, mark the row with `@container` and set `iconsOnlyBelow` on the control to the row width below which the labels no longer fit: `xs` (20rem), `sm` (24rem), `md` (28rem), `lg` (32rem), `xl` (36rem) or `2xl` (42rem). Below it, every segment becomes a 32px square that shows only its icon: its label and its count stay its accessible name, and its label shows in a tooltip on hover and on keyboard focus. The control keeps its 32px height and its text size. Set `iconsOnlyBelow` only when every segment has an icon: a segment without one would be left empty.
- Pick the width from the row, not the window: the same row sits in columns of different widths, and a container query follows the column.
- Stretch it only when it is the only choice in a form. Then give it the full width (`w-full`) on its own line, not in a row beside other controls, and let the segments share it equally: in a row, a full-width track that can't shrink pushes past its neighbors.
- On a phone, use at most 4 segments and show only their icons when they don't fit, with `iconsOnlyBelow` ([Responsive layout](https://raft-design.botiverse.build/patterns/responsive.md#phone)).

```tsx
import { useState } from "react";
import { SegmentedControl, SegmentedControlItem, SegmentedControlLabel } from "@botiverse/raft-ui";
import { CalendarDays, Columns3, List } from "lucide-react";

export function SegmentedControlNarrowRowContent() {
  const [view, setView] = useState("board");
  return (
    <div className="@container flex w-full items-center justify-between gap-3">
      <h3 className="text-headline text-foreground">Tasks</h3>
      <SegmentedControl
        aria-label="Task view"
        iconsOnlyBelow="sm"
        value={view}
        onValueChange={setView}
      >
        <SegmentedControlItem value="board">
          <Columns3 />
          <SegmentedControlLabel>Board</SegmentedControlLabel>
        </SegmentedControlItem>
        <SegmentedControlItem value="list">
          <List />
          <SegmentedControlLabel>List</SegmentedControlLabel>
        </SegmentedControlItem>
        <SegmentedControlItem value="calendar">
          <CalendarDays />
          <SegmentedControlLabel>Calendar</SegmentedControlLabel>
        </SegmentedControlItem>
      </SegmentedControl>
    </div>
  );
}
```

## Content

- Label each segment with 1 or 2 words in Title Case: Board, List; Relevant, Recent ([Typography](https://raft-design.botiverse.build/foundations/typography.md#casing)).
- Put a count in `SegmentedControlCount`, never in the label's text.

## Accessibility

- `SegmentedControl` wraps the Base UI RadioGroup, and each `SegmentedControlItem` is a Base UI Radio rendered as a button: the control is one tab stop, and the arrow keys move the selection.
- Name the control with `aria-label` after what it switches: "Task view", "Time format".
- Give every icon-only segment an `aria-label`. A segment that shows only its icon because of `iconsOnlyBelow` needs none: its label is still in it, hidden from sight but not from assistive technology.

## API

### SegmentedControl

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `iconsOnlyBelow` | `SegmentedControlIconsOnlyBelow` |  | When the nearest `@container` ancestor (the row the control sits in) is narrower than this width, every segment shows only its icon (give each one). Its label stays the accessible name and shows in a tooltip. |
| `defaultValue` | `T` |  | The uncontrolled value of the radio button that should be initially selected. |
| `disabled` | `boolean` | `false` | Whether the component should ignore user interaction. |
| `form` | `string` |  | Identifies the form that owns the radio inputs. Useful when the radio group is rendered outside the form. |
| `inputRef` | `((instance: HTMLInputElement \| null) => void \| (() => void \| { [UNDEFINED_VOID_ONLY]: never; }) \| DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) \| RefObject<HTMLInputElement \| null>` |  | A ref to access the hidden input element. |
| `name` | `string` |  | Identifies the field when a form is submitted. |
| `onValueChange` | `(value: T, eventDetails: BaseRadioGroup.ChangeEventDetails) => void` |  | Callback fired when the value changes. |
| `readOnly` | `boolean` | `false` | Whether the user should be unable to select a different radio button in the group. |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |
| `required` | `boolean` | `false` | Whether the user must choose a value before submitting a form. |
| `value` | `T` |  | The controlled value of the radio item that should be currently selected. |

Also takes the attributes of its element.

### SegmentedControlCount

Also takes the attributes of its element.

### SegmentedControlItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `disabled` | `boolean` |  | Whether the component should ignore user interaction. |
| `inputRef` | `((instance: HTMLInputElement \| null) => void \| (() => void \| { [UNDEFINED_VOID_ONLY]: never; }) \| DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) \| RefObject<HTMLInputElement \| null>` |  | A ref to access the hidden input element. |
| `nativeButton` | `boolean` | `false` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `true` if the rendered element is a native button. |
| `readOnly` | `boolean` |  | Whether the user should be unable to select the radio button. |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |
| `required` | `boolean` |  | Whether the user must choose a value before submitting a form. |
| `value` (required) | `T` |  | The unique identifying value of the radio in a group. |

Also takes the attributes of its element.

### SegmentedControlLabel

Also takes the attributes of its element.

## Changes

- 0.9.0 (Added): `SegmentedControl` takes `iconsOnlyBelow` (`"xs"` to `"2xl"`, Tailwind's container sizes; type `SegmentedControlIconsOnlyBelow`). Mark the row `@container`; below that width each segment shows only its icon as a 32px square, its label and count stay the accessible name, and a tooltip shows the label. Use it only when every segment has an icon.
- 0.8.0 (Breaking Changes): `SegmentedControl` no longer shrinks with `max-w-full`; it keeps its width (`shrink-0`). In a narrow row, give it room or let the row degrade as `responsive.md` says, or it overflows.
- 0.8.0 (Fixed): `SegmentedControl` keeps its width and no longer gets squeezed by a neighbor in the same row.
