Segmented Button
A connected group of toggle buttons for mutually exclusive (or multi-select) choices. Used for view modes, filter states, alignment, and time-range selectors.
Preview
Icon only
Omit label and provide only icon for compact toolbars. Always add a parent aria-label or title on each segment when labels are hidden.
Multi-select
Set multiple to allow any combination of segments to be active simultaneously. value becomes a string[] and each segment uses role="checkbox" instead of radio.
Sizes
Three sizes share the same scale as Button and Input.
Implementation
The selected segment is styled via a direct class comparison — no hidden input, no CSS peer trick. The active pill uses a slightly elevated surface with an inset border and a shallow box-shadow to appear "pressed in" to the track.
import { SegmentedButton } from "@/components/ui/SegmentedButton";
import { LayoutGrid, List, Map } from "lucide-react";
// Single selection (default)
const [view, setView] = useState("grid");
<SegmentedButton
value={view}
onChange={(v) => setView(v)}
options={[
{ value: "grid", label: "Grid", icon: <LayoutGrid className="w-4 h-4" /> },
{ value: "list", label: "List", icon: <List className="w-4 h-4" /> },
{ value: "map", label: "Map", icon: <Map className="w-4 h-4" /> },
]}
/>
// Multiple selection
const [formats, setFormats] = useState(["bold"]);
<SegmentedButton
multiple
value={formats}
onChange={(v) => setFormats(v)}
options={[
{ value: "bold", icon: <Bold className="w-4 h-4" /> },
{ value: "italic", icon: <Italic className="w-4 h-4" /> },
{ value: "underline", icon: <Underline className="w-4 h-4" /> },
]}
/>
// Full width
<SegmentedButton fullWidth value={period} onChange={setPeriod}
options={[
{ value: "day", label: "Day" },
{ value: "week", label: "Week" },
{ value: "month", label: "Month" },
{ value: "year", label: "Year" },
]}
/>SegmentedButton props
| Prop | Type | Default | Description |
|---|---|---|---|
options | SegmentedButtonOption[] | — | The segments to render. Each has value, optional label, optional icon. |
value | T | T[] | — | Controlled selected value. Array when multiple=true. |
onChange | (value: T | T[]) => void | — | Called with the new value when a segment is clicked. |
multiple | boolean | false | Allows multiple segments to be active simultaneously. |
size | "sm" | "md" | "lg" | "md" | Segment height and font size. |
fullWidth | boolean | false | Stretches the group to fill its container; each segment gets equal width. |
SegmentedButtonOption
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Unique identifier for this segment. |
label | string | — | Text label. Can be omitted for icon-only segments. |
icon | ReactNode | — | Icon rendered before the label. |
disabled | boolean | — | Prevents selection of this segment. |
Mobile
Segmented buttons with many segments overflow on narrow screens. Cap segments at 4 on mobile, or switch to a Select or bottom sheet for larger option sets.
Locations
| Scenario | Guidance |
|---|---|
| Segment count | Limit to 3–4 segments on mobile. Five or more segments will either overflow or produce segments too narrow to tap reliably (below 44px each). |
| Scrollable variant | If more options are needed, make the segmented button horizontally scrollable with overflow-x: auto. The selected segment should always be visible on initial render — scroll it into view. |
| Touch target height | Use h-10 (40px) or taller on mobile. The default h-8 (32px) is below the 44px target — add extra padding or switch sizes when space allows. |
| Icon-only segments | Icon-only segments save space but require a visible label elsewhere on screen to explain what mode is selected. On mobile, prefer icon + short label. |
| Alternative on mobile | For 5+ options, replace the segmented button with a native select or a custom bottom sheet picker. These are more thumb-friendly and scale to any option count. |
Accessibility
- →The group has role="radiogroup" (single) or role="group" (multiple). Each segment has role="radio" or role="checkbox" with aria-checked.
- →Add an aria-label to the SegmentedButton when the options' meaning isn't clear from context.
- →Keyboard: Tab focuses the group; Arrow keys move between segments in single-select mode.
- →Icon-only segments must have a title attribute or aria-label for screen readers.
- →Never rely solely on selected state to communicate the current view — pair with a visible heading or title.