|Kit

Segment Group

A segmented single-choice control on Ark UI's SegmentGroup machine — a compact, always-visible row of mutually exclusive segments with a sliding pill, built-in label / description / error, and native form submission.

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

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

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

Playground

Component

Loading preview

Props

Code

<Component
  orientation="horizontal"
  variant="default"
  size="md"
/>

Items

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

// 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 guide.

Align

Variants

Five treatments, mirroring 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.

Code

<SegmentGroup variant="filled" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup variant="light" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup variant="outline" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup variant="subtle" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup variant="default" data={['List', 'Grid', 'Board']} defaultValue="List" />

Sizes

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

Code

<SegmentGroup size="xs" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup size="sm" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup size="md" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup size="lg" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup size="xl" data={['List', 'Grid', 'Board']} defaultValue="List" />

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's AM/PM toggle is exactly this case.

Period

Code

<div className="flex items-end gap-2">
  <NumberInput label="Hour" size="md" defaultValue={9} min={0} max={23} />
  <SegmentGroup label="Period" size="md" data={['AM', 'PM']} defaultValue="AM" />
</div>

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.

Code

<SegmentGroup color="brand" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup color="green" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup color="blue" data={['List', 'Grid', 'Board']} defaultValue="List" />
<SegmentGroup color="purple" data={['List', 'Grid', 'Board']} defaultValue="List" />

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.

Code

<SegmentGroup
  defaultValue="active"
  data={[
    { value: 'todo', label: 'Queued' },
    { value: 'active', label: 'In progress', color: 'blue' },
    { value: 'done', label: 'Validated', color: 'green' },
  ]}
/>

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

"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

SegmentGroup is generic over data (via a const type parameter), exactly like Select: 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.

"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

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

Label, description & error

SegmentGroup wraps itself in 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.

ViewHow the projects are laid out.
View
Pick a layout

Code

<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']}
/>

Controlled

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

"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

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.

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

API

Prop

Type

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 and ButtonMultiSelect.

Changelog

v0.19.0
  • The default ThemeIcon and the filled Tabs and SegmentGroup tray use the kit's hairline border and quiet fill instead of the gray scale

    #316
  • Time-segment and AM/PM groups take their name from the field label, and DatePicker's range rows are announced as "Start time" and "End time"

    #329
  • Right to left, Tabs and SegmentGroup separators sit between the items, and Table headers align to the start with scroll fades on the correct edge

    #331
v0.9.2
  • The selected label follows its pill's palette, staying legible on colored strips and while hovered

    #143
v0.6.0
  • One size scale, plain xs–xl matching Input's height: size="input-md" becomes size="md"

    #104
  • Tint the default indicator off the group color

    #106
v0.5.1
  • Field's label names the real control through aria-labelledby

    #77
v0.5.0
  • The light and default trays use Input's translucent surface

    #63
  • Wire the field's description and error to the control with aria-describedby and aria-errormessage

    #57
v0.3.0
  • items → data on RadioGroup, ChoiceCardGroup, SegmentGroup and Menu

    #34
v0.2.0
  • input-xs–input-xl sizes that match an Input's height

    #16
v0.0.1
  • SegmentGroup

On this page