Skip to main content

Segmented Control

A row of segments that switches between a few short options, like a view or an environment. A raised thumb slides to the checked segment.

import { SegmentedControl } from "@ngrok/mantle/segmented-control"; <SegmentedControl.Root aria-label="Environment" defaultValue="production">	<SegmentedControl.Item value="development">Dev</SegmentedControl.Item>	<SegmentedControl.Item value="staging" disabled>		Staging	</SegmentedControl.Item>	<SegmentedControl.Item value="production">Prod</SegmentedControl.Item></SegmentedControl.Root>;

When to use

  • Switch between a few short options, like a view or an environment, in one row.
  • Prefer RadioGroup for a form field whose options need a description, or stack in a column.
  • Prefer Tabs when each option shows its own panel of content.
  • Prefer Select when the set is long, or the row has little space.

Controlled

Pass value and onChange to keep the checked segment in your own state. The control reports the new value on a click, a key, and a drag.

Current view: list

import { SegmentedControl } from "@ngrok/mantle/segmented-control";import { useState } from "react"; function ControlledExample() {	const [view, setView] = useState("list"); 	return (		<div className="flex flex-col items-center gap-3">			<SegmentedControl.Root aria-label="View" value={view} onChange={setView}>				<SegmentedControl.Item value="list">List</SegmentedControl.Item>				<SegmentedControl.Item value="board">Board</SegmentedControl.Item>				<SegmentedControl.Item value="calendar">Calendar</SegmentedControl.Item>			</SegmentedControl.Root>			<p className="text-muted text-sm">Current view: {view}</p>		</div>	);}

Icon segments

A segment can hold only an icon. Give each one its name in sr-only text, so a screen reader announces it.

import { SegmentedControl } from "@ngrok/mantle/segmented-control";import { ListIcon } from "@phosphor-icons/react/List";import { SquaresFourIcon } from "@phosphor-icons/react/SquaresFour"; <SegmentedControl.Root aria-label="Layout" defaultValue="list" className="w-fit">	<SegmentedControl.Item value="list">		<ListIcon aria-hidden className="size-5" />		<span className="sr-only">List</span>	</SegmentedControl.Item>	<SegmentedControl.Item value="grid">		<SquaresFourIcon aria-hidden className="size-5" />		<span className="sr-only">Grid</span>	</SegmentedControl.Item></SegmentedControl.Root>;

With TanStack Form

Use @tanstack/react-form with zod to keep the control controlled in a form. Pass the field's name, value, and handleChange. Use Field.Set and Field.Legend to name the group and show its errors.

Region
import { Button } from "@ngrok/mantle/button";import { Field, toErrorMessages } from "@ngrok/mantle/field";import { SegmentedControl } from "@ngrok/mantle/segmented-control";import { useForm } from "@tanstack/react-form";import { z } from "zod"; export const formSchema = z.object({	region: z.string().min(1, "Choose a region."),}); function TanStackFormExample() {	const form = useForm({		defaultValues: { region: "" },		validators: {			onSubmit: formSchema,		},		onSubmit: ({ value }) => {			// Handle form submission here		},	}); 	return (		<form			onSubmit={(event) => {				event.preventDefault();				event.stopPropagation();				void form.handleSubmit();			}}		>			<form.Field name="region">				{(field) => (					<Field.Set>						<Field.Legend>Region</Field.Legend>						<SegmentedControl.Root							name={field.name}							value={field.state.value}							onChange={field.handleChange}						>							<SegmentedControl.Item value="us">US</SegmentedControl.Item>							<SegmentedControl.Item value="eu">EU</SegmentedControl.Item>							<SegmentedControl.Item value="ap">AP</SegmentedControl.Item>						</SegmentedControl.Root>						<Field.Errors messages={toErrorMessages(field.state.meta.errors)} />					</Field.Set>				)}			</form.Field>			<Button type="submit" appearance="filled" intent="neutral">				Submit			</Button>		</form>	);}

Composition

SegmentedControl.Root└── SegmentedControl.Item

Radio semantics

SegmentedControl is a radio group underneath. It shares its primitive with RadioGroup, so it has the same contract: role="radiogroup", one tab stop, arrow keys that move the checked segment, and a hidden <input type="radio"> for form submission when it has a name.

To change the value, click a segment, use the arrow keys, or drag the thumb with a mouse or a finger.

Dragging the thumb

A sideways drag on the checked segment moves the thumb with the pointer. On release, the thumb slides to the nearest enabled segment, and that segment becomes checked. A flick carries the thumb further. Past either end, the thumb meets resistance and stays inside the track. A drag that goes up or down more than sideways does not start, so the page still scrolls on a touch screen.

The thumb slides on every change: a click, a key, a drag, or a new value. With reduced motion, it moves at once.

API Reference

SegmentedControl.Root

The row of segments. It renders the radio group itself, so use it in place of RadioGroup.Root, never inside one.

All props from Headless UI RadioGroup, including:

PropTypeDefaultDescription
value?stringThe controlled value of the checked segment.
defaultValue?stringThe value checked at first, when uncontrolled.
onChange?(value: string) => voidCalled with the new value when the checked segment changes.
disabled?booleanfalseWhen true, prevents interaction with every segment.
name?stringThe name of the hidden radio input that a form submits.
aria-label?stringThe name of the control. Set it, or set aria-labelledby.
Data AttributeValueDescription
data-slot"segmented-control"The control.
data-draggingpresent during a dragThe thumb follows the pointer. Presence-only.

SegmentedControl.Item

One segment. It grows to share the row and keeps its label on one line, so keep labels to one or two words. Inside Field.Control, the segment picks up aria-invalid, aria-errormessage, and aria-describedby.

PropTypeDefaultDescription
valuestringThe value the control reports when this segment is checked.
disabled?booleanWhen true, the segment cannot be checked.
Data AttributeValueDescription
data-slot"segmented-control-item"The segment.
data-thumb-overpresent during a dragThe thumb is over this segment. Presence-only.