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
Dir
Presets
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.
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.
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.
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,Tabenters / leaves the group, and the selection follows focus. - Each segment is backed by a hidden native radio input with the shared
name, exposingrole="radiogroup"/radioandaria-checked. invalid(orerror) flips the group to the red state; label / description / error associations are wired via the built-inField.- 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
- #316
The
defaultThemeIcon and thefilledTabs and SegmentGroup tray use the kit's hairline border and quiet fill instead of the gray scale - #329
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"
- #331
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
- #143
The selected label follows its pill's palette, staying legible on colored strips and while hovered
- #77
Field's label names the real control through
aria-labelledby
- #34
items→dataon RadioGroup, ChoiceCardGroup, SegmentGroup and Menu
- #16
input-xs–input-xlsizes that match an Input's height
SegmentGroup
Switch
A single boolean toggle on Ark UI's Switch machine — sliding track + thumb, inline label, built-in description / error, native form submission, and full keyboard / ARIA.
Button Select
Pick one value from a row of joined Buttons — the selected one flips to its own variant. Built on Ark UI's ToggleGroup machine, with label / description / error and native form submission.