# SegmentedControl

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

One choice out of a few short possibilities, drawn as one control with a segment for each (segmented button, button toggle): a sunken track, the choice lifted out of it. As tall as a select, so it stands in a row of fields too.

```ts
import { SegmentedControl } from "@umriss-ui/core";
```

Demo page: https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/

A screen reader hears a radio group, each segment a radio with its position and whether it is chosen; the control is one tab stop, and the arrow keys move and choose at once, skipping disabled possibilities.

Each segment is as wide as its word, and the width never follows the choice. `fill` takes the width of the place instead and shares it equally; an option's `icon` stands before its word.

Two drawings: `inset`, the default - a sunken track with the choice lifted out of it; and `field` - a field's edge, the choice filled with ink, for a row whose fields it should match box for box. Both are a control's height with their words on a select's line, and the keys are the same.

## Examples

### Segmented control

Pass `options`, `value` and `onChange`; `value` is `null` until something is chosen.

```tsx
import { useState } from "react";
import { SegmentedControl, Text, Stack } from "@umriss-ui/core";

export default function SegmentedControlExample() {
  const [period, setPeriod] = useState<"hour" | "day" | "week" | null>("day");

  return (
    <Stack gap={3} align="flex-start">
      <SegmentedControl
        aria-label="Period"
        value={period}
        onChange={setPeriod}
        options={[
          { value: "hour", label: "Hour" },
          { value: "day", label: "Day" },
          { value: "week", label: "Week" },
        ]}
      />
      <Text size="xs" tone="muted" mono>
        period: {period ?? "none"}
      </Text>
    </Stack>
  );
}
```

### In a field

Inside a `FormField` the control is named by the field's label and takes its hint or error, required and invalid state by itself.

```tsx
import { useState } from "react";
import { FormField, SegmentedControl } from "@umriss-ui/core";

export default function InAField() {
  const [state, setState] = useState<"raw" | "cleaned">("raw");

  return (
    <FormField
      label="State of the data"
      required
      error={state === "raw" ? "A report is drawn from cleaned data." : undefined}
    >
      <SegmentedControl
        value={state}
        onChange={setState}
        options={[
          { value: "raw", label: "Raw data" },
          { value: "cleaned", label: "Cleaned data" },
        ]}
      />
    </FormField>
  );
}
```

### States and sizes

`size` is `md` or `sm`; a `ControlSizeProvider` sets it for a place. `disabled` closes the whole control, `disabled` on an option closes that possibility and leaves it in view - the arrow keys skip it.

```tsx
import { FormField, SegmentedControl, Stack } from "@umriss-ui/core";

const PERIODS = [
  { value: "hour", label: "Hour" },
  { value: "day", label: "Day" },
  { value: "week", label: "Week" },
] as const;

export default function StatesAndSizes() {
  return (
    <Stack direction="row" gap={5} wrap>
      <FormField label="Medium">
        <SegmentedControl defaultValue="day" options={PERIODS} />
      </FormField>
      <FormField label="Small">
        <SegmentedControl size="sm" defaultValue="day" options={PERIODS} />
      </FormField>
      <FormField label="Disabled">
        <SegmentedControl disabled defaultValue="day" options={PERIODS} />
      </FormField>
      <FormField label="One possibility closed" hint="No forecast for this line yet.">
        <SegmentedControl
          defaultValue="measured"
          options={[
            { value: "measured", label: "Measured" },
            { value: "forecast", label: "Forecast", disabled: true },
          ]}
        />
      </FormField>
    </Stack>
  );
}
```

### Long words in a narrow place

The control is as wide as its words and never wider than its place: on a phone, words that do not fit end in an ellipsis. Short words are the form's strength - past three or four of them, a Select is the better choice.

```tsx
import { FormField, SegmentedControl } from "@umriss-ui/core";

export default function LongWordsInANarrowPlace() {
  return (
    <FormField label="Readings">
      <SegmentedControl
        defaultValue="raw"
        options={[
          { value: "raw", label: "Raw readings from the sensors" },
          { value: "cleaned", label: "Cleaned and validated readings" },
        ]}
      />
    </FormField>
  );
}
```

### A theme switch

A choice that stands on its own, in a menu or a settings panel. `fill` takes the panel's width and shares it equally between the segments; an option's `icon` stands before its word, at the size the control gives it.

```tsx
import { Card, CardBody, FormField, SegmentedControl } from "@umriss-ui/core";

/* Icons from outside the set: any SVG, sized by the control. */
const stroke = { fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" } as const;

function Sun() {
  return (
    <svg viewBox="0 0 24 24" {...stroke}>
      <circle cx="12" cy="12" r="4" />
      <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
    </svg>
  );
}

function Moon() {
  return (
    <svg viewBox="0 0 24 24" {...stroke}>
      <path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z" />
    </svg>
  );
}

function Monitor() {
  return (
    <svg viewBox="0 0 24 24" {...stroke}>
      <rect x="3" y="4" width="18" height="12" rx="2" />
      <path d="M8 20h8M12 16v4" />
    </svg>
  );
}

export default function ThemeSwitch() {
  return (
    <Card style={{ maxWidth: 320 }}>
      <CardBody>
        <FormField label="Theme">
          <SegmentedControl
            fill
            defaultValue="system"
            options={[
              { value: "light", label: "Light", icon: <Sun /> },
              { value: "dark", label: "Dark", icon: <Moon /> },
              { value: "system", label: "System", icon: <Monitor /> },
            ]}
          />
        </FormField>
      </CardBody>
    </Card>
  );
}
```

### Drawn as a field

`variant="field"` gives it a field's edge and fills the choice with ink, for a row whose fields it should match box for box. Inset or a field, it is a control's height and its words stand on the select's line - at either size.

```tsx
import { ControlSizeProvider, FormField, SegmentedControl, Select, Stack } from "@umriss-ui/core";
import type { ControlSize } from "@umriss-ui/core";

function Row({ size }: { size: ControlSize }) {
  return (
    <Stack role="group" aria-label={size === "sm" ? "Small" : "Medium"} direction="row" gap={3} wrap>
      <FormField label="Caster">
        <Select defaultValue="">
          <option value="">Every caster</option>
          <option>Caster 1</option>
        </Select>
      </FormField>
      <FormField label="Values">
        <SegmentedControl
          variant="field"
          defaultValue="measured"
          options={[
            { value: "measured", label: "Measured" },
            { value: "forecast", label: "Forecast" },
          ]}
        />
      </FormField>
    </Stack>
  );
}

export default function DrawnAsAField() {
  return (
    <Stack gap={5}>
      <Row size="md" />
      <ControlSizeProvider size="sm">
        <Row size="sm" />
      </ControlSizeProvider>
    </Stack>
  );
}
```

## When to use something else

- Possibilities that need a line of explanation each → RadioGroup
- More than about four possibilities, or long ones → Select
- A single yes or no → Switch

## Keyboard

| Key | Action |
|---|---|
| `Tab` | Moves into the control, onto the chosen segment, and out again. |
| `Arrow Right / Arrow Down` | Chooses the next segment, wrapping at the end. |
| `Arrow Left / Arrow Up` | Chooses the previous segment. |

## API

### `SegmentedControlProps<T extends string>`

| Prop | Type | Default | Description |
|---|---|---|---|
| `options` *required* | `readonly `[`SegmentedOption`](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#type-SegmentedOption)`<T>[]` | — | The possibilities, two to four, in the order in which they stand.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#configurator-segmentedcontrol), [Segmented control](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#segmented-control), [In a field](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#in-a-field) and 5 more |
| `defaultValue` | `T \| null` | — | Initial value of an uncontrolled control. Not together with `value`.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#configurator-segmentedcontrol), [States and sizes](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#states-and-sizes), [Long words in a narrow place](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#long-words-in-a-narrow-place) and 3 more |
| `value` | `T \| null` | — | The chosen possibility; `null` means: none yet. Together with `onChange` the control is controlled.<br>Shown in: [Segmented control](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#segmented-control), [In a field](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#in-a-field) |
| `size` | `"sm" \| "md"` | the size of a `ControlSizeProvider` around it, else `"md"` | `sm` for a toolbar and dense forms, `md` otherwise.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#configurator-segmentedcontrol), [States and sizes](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#states-and-sizes) |
| `variant` | `"field" \| "inset"` | `"inset"` | How it is drawn. `inset`: a sunken track without an edge, the choice a surface lifted out of it. `field`: a field's edge, the choice filled with ink - for a row whose fields it should match box for box.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#configurator-segmentedcontrol), [Drawn as a field](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#drawn-as-a-field) |
| `fill` | `boolean` | `false` | Takes the whole width its place gives it, as a field in a column does, and shares it equally between the segments. Without it the control is as wide as its words.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#configurator-segmentedcontrol), [A theme switch](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#a-theme-switch) |
| `disabled` | `boolean` | `false` | Disables the whole control. `SegmentedOption` disables single possibilities.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#configurator-segmentedcontrol), [States and sizes](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#states-and-sizes) |
| `name` | `string` | a generated one | Name of the radio group within the form. |
| `ref` | `Ref<HTMLDivElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| `onChange` | `(value: T) => void` | — | Runs on every choice – on the one made with the arrow keys as well.<br>Shown in: [Segmented control](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#segmented-control), [In a field](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#in-a-field) |

Also takes every attribute of `<div>` – without `defaultValue`, `onChange`.

### `SegmentedOption<T extends string>`

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` *required* | `T` | — | What comes back when this possibility is chosen.<br>Shown in: [Segmented control](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#segmented-control), [In a field](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#in-a-field), [States and sizes](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#states-and-sizes) and 4 more |
| `label` *required* | `ReactNode` | — | The segment's word - a short one; a long label ends in an ellipsis.<br>Shown in: [Segmented control](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#segmented-control), [In a field](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#in-a-field), [States and sizes](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#states-and-sizes) and 4 more |
| `icon` | `ReactNode` | — | An icon before the word: an SVG, a glyph of the set or an icon font's element. The control sets its size - `--u-icon-size`, `--u-icon-size-sm` at `sm` - and hides it from assistive technology: the word names the possibility.<br>Shown in: [A theme switch](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#a-theme-switch) |
| `disabled` | `boolean` | — | Takes the possibility out of the choice but leaves it visible.<br>Shown in: [States and sizes](https://romanhaendler.github.io/umriss-ui/core/segmentedcontrol/#states-and-sizes) |

## Known limits

- Two to four short possibilities: a word that does not fit its place ends in an ellipsis.
- No icons and no line of explanation in a segment; several chosen at once is not this control.

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
