# Segmented Control

A row of mutually exclusive options with a sliding indicator, for switching views.

Source: [segmented-control.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/segmented-control.tsx)

```tsx
import { GridIcon, KanbanIcon, ListIcon } from "lucide-react";

import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control";

export default function SegmentedControlWithIcons() {
  return (
    <SegmentedControl defaultValue="list" aria-label="Layout">
      <SegmentedControlItem value="list">
        <ListIcon />
        List
      </SegmentedControlItem>
      <SegmentedControlItem value="grid">
        <GridIcon />
        Grid
      </SegmentedControlItem>
      <SegmentedControlItem value="board">
        <KanbanIcon />
        Board
      </SegmentedControlItem>
    </SegmentedControl>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/segmented-control
```

## Usage

```tsx
import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control";

export function RangePicker() {
  return (
    <SegmentedControl defaultValue="week" aria-label="Range">
      <SegmentedControlItem value="day">Day</SegmentedControlItem>
      <SegmentedControlItem value="week">Week</SegmentedControlItem>
      <SegmentedControlItem value="month">Month</SegmentedControlItem>
    </SegmentedControl>
  );
}
```

Once a segment is chosen, one always is. Give the control an `aria-label`, or a `FieldLabel` in a
`Field`.

## Composition

- `SegmentedControl`
  - `SegmentedControlItem`

## Examples

### Controlled

Pass `value` and `onValueChange` to control the selection.

```tsx
import * as React from "react";

import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control";

export default function SegmentedControlControlled() {
  const [metric, setMetric] = React.useState("cost");
  return (
    <div className="flex flex-wrap items-center gap-3">
      <SegmentedControl value={metric} onValueChange={setMetric} aria-label="Chart metric">
        <SegmentedControlItem value="cost">Cost</SegmentedControlItem>
        <SegmentedControlItem value="tokens">Tokens</SegmentedControlItem>
      </SegmentedControl>
      <p className="text-sm text-muted-foreground">
        Showing {metric === "cost" ? "cost" : "token usage"}
      </p>
    </div>
  );
}
```

### Icon only

Give icon-only items an `aria-label`. `size="sm"` makes a smaller control.

```tsx
import { GridIcon, KanbanIcon, ListIcon } from "lucide-react";

import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control";

export default function SegmentedControlIconOnly() {
  return (
    <SegmentedControl size="sm" defaultValue="list" aria-label="Layout">
      <SegmentedControlItem value="list" aria-label="List">
        <ListIcon />
      </SegmentedControlItem>
      <SegmentedControlItem value="grid" aria-label="Grid">
        <GridIcon />
      </SegmentedControlItem>
      <SegmentedControlItem value="board" aria-label="Board">
        <KanbanIcon />
      </SegmentedControlItem>
    </SegmentedControl>
  );
}
```

### Disabled options

```tsx
import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control";

export default function SegmentedControlDisabledOption() {
  return (
    <SegmentedControl defaultValue="week" aria-label="Range">
      <SegmentedControlItem value="day">Day</SegmentedControlItem>
      <SegmentedControlItem value="week">Week</SegmentedControlItem>
      <SegmentedControlItem value="month">Month</SegmentedControlItem>
      <SegmentedControlItem value="year" disabled>
        Year
      </SegmentedControlItem>
    </SegmentedControl>
  );
}
```

### Full width

Give the control a width and the segments stretch to fill it.

```tsx
import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control";

export default function SegmentedControlFullWidth() {
  return (
    <SegmentedControl
      defaultValue="monthly"
      aria-label="Billing period"
      className="w-full max-w-sm"
    >
      <SegmentedControlItem value="monthly">Monthly</SegmentedControlItem>
      <SegmentedControlItem value="yearly">Yearly</SegmentedControlItem>
    </SegmentedControl>
  );
}
```

### In a form

Give a `Field` or the control a `name`, and the chosen value submits under it, or pass `form` with
a form's `id` when the control sits outside it. Leave out `defaultValue` to make people choose, and
with `required` the form won't submit until they do, which fails as `valueMissing`. A form reset
brings back `defaultValue`. In a `Field`, a `FieldLabel` names the control, and it reads out the
field's `FieldDescription` and `FieldError` and turns invalid or disabled with the field.

```tsx
import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control";
import { toast } from "@/components/ui/toast";

export default function SegmentedControlForm() {
  return (
    <Form
      className="max-w-sm"
      onFormSubmit={(values) =>
        toast.add({ title: "Plan updated", description: `Billed ${values.billing}.` })
      }
    >
      <Field name="billing">
        <FieldLabel>Billing</FieldLabel>
        <SegmentedControl required>
          <SegmentedControlItem value="monthly">Monthly</SegmentedControlItem>
          <SegmentedControlItem value="yearly">Yearly</SegmentedControlItem>
        </SegmentedControl>
        <FieldDescription>Yearly billing saves two months.</FieldDescription>
        <FieldError match="valueMissing">Choose how you&apos;re billed.</FieldError>
      </Field>
      <Button type="submit" variant="primary" className="self-start">
        Update plan
      </Button>
    </Form>
  );
}
```

## Props

Unlike Toggle Group, its value is a single string rather than an array.

| Part | Extends |
| --- | --- |
| `SegmentedControl` | [`ToggleGroup`](https://base-ui.com/react/components/toggle-group#api-reference) |
| `SegmentedControlItem` | [`Toggle`](https://base-ui.com/react/components/toggle#api-reference) |

The tables leave out `className`, `style` and `render`. Parts built on Base UI take them as its [styling](https://base-ui.com/react/handbook/styling) and [composition](https://base-ui.com/react/handbook/composition) guides describe.

### SegmentedControl

| Prop             | Type                         | Description                                                                                                                           |
| ---------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `disabled?`      | `boolean`                    | Whether the toggle group should ignore user interaction. Default: `false`                                                             |
| `orientation?`   | `"horizontal" \| "vertical"` | Default: `'horizontal'`                                                                                                               |
| `loopFocus?`     | `boolean`                    | Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. Default: `true` |
| `value?`         | `string \| null`             | The value of the selected segment. Once one is chosen, one always is. `null` when none is yet.                                        |
| `defaultValue?`  | `string`                     |                                                                                                                                       |
| `onValueChange?` | `((value: string) => void)`  | Called with the segment chosen, and with `defaultValue` when the control's form resets.                                               |
| `size?`          | `"default" \| "sm"`          | Default: `"default"`                                                                                                                  |
| `name?`          | `string`                     | Submits the selected value with a form under this name.                                                                               |
| `form?`          | `string`                     | The `id` of the form to submit the value with, when the control isn't inside it.                                                      |
| `required?`      | `boolean`                    | Whether a form can't be submitted until a segment is chosen. Default: `false`                                                         |

### SegmentedControlItem

| Prop               | Type                                                                    | Description                                                                                                                                                                                       |
| ------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pressed?`         | `boolean`                                                               | Whether the toggle button is currently pressed. This is the controlled counterpart of `defaultPressed`.                                                                                           |
| `defaultPressed?`  | `boolean`                                                               | Whether the toggle button is currently pressed. This is the uncontrolled counterpart of `pressed`. Default: `false`                                                                               |
| `disabled?`        | `boolean`                                                               | Whether the component should ignore user interaction. Default: `false`                                                                                                                            |
| `onPressedChange?` | `((pressed: boolean, eventDetails: Toggle.ChangeEventDetails) => void)` | Callback fired when the pressed state is changed.                                                                                                                                                 |
| `value?`           | `string`                                                                | A unique string that identifies the toggle when used inside a toggle group.                                                                                                                       |
| `nativeButton?`    | `boolean`                                                               | 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>`). Default: `true` |
