SegmentedControl

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.

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

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.

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.

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.

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.

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.

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.

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

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>

NameTypeDefaultDescription
optionsrequiredreadonly SegmentedOption<T>[]—The possibilities, two to four, in the order in which they stand.

Shown in: Configurator, Segmented control, In a field and 5 more

defaultValueT | null—Initial value of an uncontrolled control. Not together with value.

Shown in: Configurator, States and sizes, Long words in a narrow place and 3 more

valueT | null—The chosen possibility; null means: none yet. Together with onChange the control is controlled.

Shown in: Segmented control, In a field

size"sm" | "md"the size of a ControlSizeProvider around it, else "md"sm for a toolbar and dense forms, md otherwise.

Shown in: Configurator, 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.

Shown in: Configurator, Drawn as a field

fillbooleanfalseTakes 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.

Shown in: Configurator, A theme switch

disabledbooleanfalseDisables the whole control. SegmentedOption disables single possibilities.

Shown in: Configurator, States and sizes

namestringa generated oneName of the radio group within the form.
refRef<HTMLDivElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Events

NameTypeDefaultDescription
onChange(value: T) => void—Runs on every choice – on the one made with the arrow keys as well.

Shown in: Segmented control, In a field

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

SegmentedOption<T extends string>

NameTypeDefaultDescription
valuerequiredT—What comes back when this possibility is chosen.

Shown in: Segmented control, In a field, States and sizes and 4 more

labelrequiredReactNode—The segment's word - a short one; a long label ends in an ellipsis.

Shown in: Segmented control, In a field, States and sizes and 4 more

iconReactNode—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.

Shown in: A theme switch

disabledboolean—Takes the possibility out of the choice but leaves it visible.

Shown in: States and sizes

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index