# Segment Group (/docs/segment-group)





`SegmentGroup` is a compact, always-visible single-choice control — a row (or column) of mutually exclusive segments with a pill that slides to the selected item. Reach for it over a [`Select`](/docs/select) when the option set is small (2–5 items) and you want every choice visible at a glance: view toggles, filters, billing cadence, alignment. It implements the kit's value-based form-control contract, so `name` submits and `onChange` hands you the value directly. Label, description, and error are built in via [`Field`](/docs/field).

```tsx
import { SegmentGroup } from "@42-central/ui-react/segment-group";

<SegmentGroup data={["List", "Grid", "Board"]} defaultValue="List" />;
```

## Playground [#playground]

<story.Playground />

## Items [#items]

`data` accepts bare strings (where the value *is* the label) or
`{ value, label, disabled? }` objects — mixed freely.

```tsx
// 1 — strings (value === label)
<SegmentGroup data={['Common Core', 'Piscine']} />

// 2 — objects, with optional per-item disabled
<SegmentGroup
  data={[
    { value: 'left', label: 'Left' },
    { value: 'center', label: 'Center' },
    { value: 'justify', label: 'Justify', disabled: true },
  ]}
/>
```

Each item also accepts an optional `testId`, defaulting to `` `item-${value}` `` — see the [Testing](/docs/testing) guide.

<story.Preview variant="Disabled item" />

## Variants [#variants]

Five treatments, mirroring [`Button`](/docs/button). The variant restyles all three layers — the tray, the sliding pill, and the checked label — off the same `--c-*` color slots, so they all swap palettes uniformly. `filled` is a solid pill, `light` a soft tint, `outline` a ringed pill over the track, `subtle` is chromeless. `default` is the same solid pill as `filled` on a lighter tray — with no `color` set it resolves to black (white in dark theme), and it tints like every other variant when you give it one, whether that comes from the group `color` or a [per-item color](#per-item-color).

<Preview align="center" code="&#x22;<SegmentGroup variant=\&#x22;filled\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup variant=\&#x22;light\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup variant=\&#x22;outline\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup variant=\&#x22;subtle\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup variant=\&#x22;default\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />&#x22;">
  <div className="flex flex-col items-start gap-3">
    <SegmentGroup variant="filled" color="neutral" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup variant="light" color="neutral" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup variant="outline" color="neutral" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup variant="subtle" color="neutral" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup variant="default" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />
  </div>
</Preview>

## Sizes [#sizes]

Five sizes — `xs` through `xl` — matching the rest of the family.

<Preview align="center" code="&#x22;<SegmentGroup size=\&#x22;xs\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup size=\&#x22;sm\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup size=\&#x22;md\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup size=\&#x22;lg\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup size=\&#x22;xl\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />&#x22;">
  <div className="flex flex-col items-center gap-3">
    <SegmentGroup size="xs" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup size="sm" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup size="md" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup size="lg" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup size="xl" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />
  </div>
</Preview>

### Composing with Input [#composing-with-input]

Every size matches `Input` / `NumberInput`'s exact box height, so a `SegmentGroup` sits flush inline next to one of the same size — [`TimeInput`](/docs/time-input)'s AM/PM toggle is exactly this case.

<Preview align="center" code="&#x22;<div className=\&#x22;flex items-end gap-2\&#x22;>\n  <NumberInput label=\&#x22;Hour\&#x22; size=\&#x22;md\&#x22; defaultValue={9} min={0} max={23} />\n  <SegmentGroup label=\&#x22;Period\&#x22; size=\&#x22;md\&#x22; data={['AM', 'PM']} defaultValue=\&#x22;AM\&#x22; />\n</div>&#x22;">
  <div className="flex items-end gap-2">
    <NumberInput label="Hour" size="md" defaultValue="9" min="0" max="23" />

    <SegmentGroup label="Period" size="md" data="[&#x22;AM&#x22;, &#x22;PM&#x22;]" defaultValue="AM" />
  </div>
</Preview>

## Colors [#colors]

The resting segments are palette-independent; only the sliding pill (and the checked label's text) adopt `color`. Set `data-color` once on the root and every kit palette tints for free.

<Preview align="center" code="&#x22;<SegmentGroup color=\&#x22;brand\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup color=\&#x22;green\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup color=\&#x22;blue\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />\n<SegmentGroup color=\&#x22;purple\&#x22; data={['List', 'Grid', 'Board']} defaultValue=\&#x22;List\&#x22; />&#x22;">
  <div className="flex flex-col items-start gap-3">
    <SegmentGroup color="brand" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup color="green" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup color="blue" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />

    <SegmentGroup color="purple" data="[&#x22;List&#x22;, &#x22;Grid&#x22;, &#x22;Board&#x22;]" defaultValue="List" />
  </div>
</Preview>

## Per-item color [#per-item-color]

Give an item its own `color` and the pill adopts it whenever that item is selected — handy for status pickers where each option owns a hue. This overrides the group `color` for that segment, on every variant including `default`; the checked label follows the pill onto `--c-on-solid`, so it stays legible in each item's palette. Select each segment below to see the pill shift.

<Preview align="center" code="&#x22;<SegmentGroup\n  defaultValue=\&#x22;active\&#x22;\n  data={[\n    { value: 'todo', label: 'Queued' },\n    { value: 'active', label: 'In progress', color: 'blue' },\n    { value: 'done', label: 'Validated', color: 'green' },\n  ]}\n/>&#x22;">
  <SegmentGroup
    defaultValue="active"
    data="[
    { value: &#x22;todo&#x22;, label: &#x22;Todo&#x22; },
    { value: &#x22;active&#x22;, label: &#x22;Active&#x22;, color: &#x22;blue&#x22; },
    { value: &#x22;done&#x22;, label: &#x22;Done&#x22;, color: &#x22;green&#x22; },
  ]"
  />
</Preview>

## Custom rendering [#custom-rendering]

By default each segment renders its `label`. Pass `renderItem` to fill the label content yourself — it receives the (value-narrowed) `item`, including its typed [`payload`](#typed-payload), and the live `{ checked, disabled }` state. The segment keeps its sizing, sliding pill, and selection wiring; you fill the inside. Define the function in a `"use client"` module so it survives the server → client boundary.

<story.Preview variant="Custom rendering" />

```tsx
"use client";

// item.payload is inferred from `data` — here { count: number } | undefined
const renderTab = (item, { checked }) => (
  <span className="inline-flex items-center gap-1.5">
    {item.label}
    {item.payload && (
      <span
        className={
          checked
            ? "bg-(--c-on-solid)/20 rounded-full px-1.5"
            : "rounded-full bg-gray-light-200 px-1.5"
        }
      >
        {item.payload.count}
      </span>
    )}
  </span>
);

<SegmentGroup data={inbox} renderItem={renderTab} />;
```

### Typed payload [#typed-payload]

`SegmentGroup` is generic over `data` (via a `const` type parameter), exactly like [`Select`](/docs/select#typed-payload): `value` / `onChange` narrow to the literal value union, and an item's `payload` is inferred so `item.payload` is fully typed in `renderItem` (and `undefined` for bare-string or payload-less items). The machine ignores `payload` — only your renderer sees it.

```tsx
"use client";

const evals = [
  { value: "all", label: "All", payload: { count: 128 } },
  { value: "booked", label: "Booked", payload: { count: 9 } },
] as const;

// value is 'all' | 'booked'; item.payload is { count: number } | undefined
<SegmentGroup
  data={evals}
  defaultValue="all"
  onChange={(value) => console.log(value)}
  renderItem={(item) => (
    <>
      {item.label} ({item.payload?.count})
    </>
  )}
/>;
```

## Orientation [#orientation]

Set `orientation="vertical"` to stack the segments in a column instead of a row.

<story.Preview variant="Vertical" />

## Label, description & error [#label-description--error]

`SegmentGroup` wraps itself in [`Field`](/docs/field), so `label` / `description` / `error` / `required` work out of the box and wire the `aria-*` relationships. Passing `error` flips the control to the invalid (red) state.

<Preview align="center" code="&#x22;<SegmentGroup\n  label=\&#x22;View\&#x22;\n  description=\&#x22;How the projects are laid out.\&#x22;\n  data={['List', 'Grid', 'Board']}\n  defaultValue=\&#x22;Grid\&#x22;\n/>\n\n<SegmentGroup\n  label=\&#x22;View\&#x22;\n  error=\&#x22;Pick a layout\&#x22;\n  data={['List', 'Grid', 'Board']}\n/>&#x22;">
  <div className="flex flex-col gap-5">
    <SegmentGroup label="View" description="How the projects are laid out." data="['List', 'Grid', 'Board']" defaultValue="Grid" />

    <SegmentGroup label="View" error="Pick a layout" data="['List', 'Grid', 'Board']" />
  </div>
</Preview>

## Controlled [#controlled]

Drive the value from state. `onChange` hands you the selected value directly.

```tsx
"use client";
import { useState } from "react";
import { SegmentGroup } from "@42-central/ui-react/segment-group";

export function ViewToggle() {
  const [view, setView] = useState("list");
  return (
    <SegmentGroup
      data={["List", "Grid", "Board"]}
      value={view}
      onChange={setView}
    />
  );
}
```

## Forms [#forms]

`SegmentGroup` renders a hidden radio input per segment, all sharing the field `name`, so the selected value rides a native `<form>` submission and `FormData` — no controlled state required.

```tsx
<form action={save}>
  <SegmentGroup name="view" data={["List", "Grid", "Board"]} />
</form>
```

## API [#api]

<TypeTable
  type="{
  data: {
    type: &#x22;string[] | { value, label, disabled?, color? }[]&#x22;,
    description:
      &#x22;Segments. Bare strings (value === label), or objects. A per-item color tints the pill when that item is selected (even in the default variant).&#x22;,
  },
  value: {
    type: &#x22;string | null&#x22;,
    description: &#x22;Controlled value.&#x22;,
  },
  defaultValue: {
    type: &#x22;string | null&#x22;,
    description: &#x22;Uncontrolled initial value.&#x22;,
  },
  onChange: {
    type: &#x22;(value: string) => void&#x22;,
    description:
      &#x22;Fires with the selected value (narrowed to the data value union).&#x22;,
  },
  renderItem: {
    type: &#x22;(item, state) => ReactNode&#x22;,
    description:
      'Fill each segment label yourself. item is { value, label, disabled, color, payload }; state is { checked, disabled }. Define it in a &#x22;use client&#x22; module.',
  },
  orientation: {
    type: '&#x22;horizontal&#x22; | &#x22;vertical&#x22;',
    default: '&#x22;horizontal&#x22;',
    description: &#x22;Lay the segments in a row or a column.&#x22;,
  },
  variant: {
    type: '&#x22;filled&#x22; | &#x22;light&#x22; | &#x22;outline&#x22; | &#x22;subtle&#x22; | &#x22;default&#x22;',
    default: '&#x22;default&#x22;',
    description:
      &#x22;Visual treatment, mirroring Button. Styles the tray, pill, and checked label.&#x22;,
  },
  size: {
    type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;',
    default: '&#x22;md&#x22;',
    description:
      &#x22;Segment sizing. Matches the exact box height of an Input / NumberInput at that size, so a SegmentGroup composes flush inline next to one.&#x22;,
  },
  color: {
    type: &#x22;Color&#x22;,
    default: '&#x22;brand&#x22;',
    description: &#x22;Pill fill + checked-label accent.&#x22;,
  },
  invalid: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Red invalid state, OR'd with error.&#x22;,
  },
  disabled: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Disable the whole group.&#x22;,
  },
  readOnly: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Keep contrast but reject changes.&#x22;,
  },
  label: { type: &#x22;ReactNode&#x22;, description: &#x22;Field label.&#x22; },
  description: { type: &#x22;ReactNode&#x22;, description: &#x22;Field helper text.&#x22; },
  error: {
    type: &#x22;ReactNode&#x22;,
    description: &#x22;Field error; sets the invalid state.&#x22;,
  },
  required: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Required flag + asterisk.&#x22;,
  },
  name: {
    type: &#x22;string&#x22;,
    description: &#x22;Name for the hidden radio inputs (form submission).&#x22;,
  },
  classNames: {
    type: &#x22;{ field?, label?, root?, indicator?, item?, itemText? }&#x22;,
    description: &#x22;Per-part class overrides.&#x22;,
  },
  testId: {
    type: &#x22;string&#x22;,
    description:
      &#x22;data-testid on the group root. See the per-item testId note in Items above for the segments themselves.&#x22;,
  },
}"
/>

## Accessibility [#accessibility]

* Built on Ark UI's SegmentGroup (radio-group) machine: `←` / `→` (or `↑` / `↓` when vertical) move and select, `Tab` enters / leaves the group, and the selection follows focus.
* Each segment is backed by a hidden native radio input with the shared `name`, exposing `role="radiogroup"` / `radio` and `aria-checked`.
* `invalid` (or `error`) flips the group to the red state; label / description / error associations are wired via the built-in `Field`.
* RTL is honored via the document `dir` — the segments and the sliding pill mirror automatically.

For options that should look like buttons (icons, per-item colors, toolbars), or for several selected values at once, see [`ButtonSelect`](/docs/button-select) and [`ButtonMultiSelect`](/docs/button-multi-select).
