|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/ui-react/segment-group';

<SegmentGroup
  items={['List', 'Grid', 'Board']}
  defaultValue="List"
/>

Playground

Component

Loading preview

Props

Code

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

Items

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

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

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

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 intentionally palette-independent (a neutral pill); the group color only tints the other four — though a per-item color still tints default.

Code

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

Sizes

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

Code

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

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" items={['List', 'Grid', 'Board']} defaultValue="List" /><SegmentGroup color="green" items={['List', 'Grid', 'Board']} defaultValue="List" /><SegmentGroup color="blue" items={['List', 'Grid', 'Board']} defaultValue="List" /><SegmentGroup color="purple" items={['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, and unlike the group color it tints even the neutral default variant (the pill turns into a solid fill in that color). Select each segment below to see the pill shift.

Code

<SegmentGroupdefaultValue="active"items={[  { 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 `items` — 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 items={inbox} renderItem={renderTab} />

Typed payload

SegmentGroup is generic over items (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
  items={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.

How the projects are laid out.
Pick a layout

Code

<SegmentGrouplabel="View"description="How the projects are laid out."items={['List', 'Grid', 'Board']}defaultValue="Grid"/><SegmentGrouplabel="View"error="Pick a layout"items={['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/ui-react/segment-group';

export function ViewToggle() {
  const [view, setView] = useState('list');
  return (
    <SegmentGroup
      items={['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" items={['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.

On this page