React Segmented Control: iOS Thumb, Drag and When to Use It
A React segmented control is the row of two to five options with a raised thumb under the selected one, the control iOS uses to pick the time range of the charts in the Health app. Search for an iOS segmented control web component and most results are React Native packages, and shadcn/ui ships none as of October 2026. This guide covers what the web version has to get right (the thumb, touch, keyboard and radio semantics), then the decision that matters more: when Tabs, a Toggle Group or radio cards are the better control. The examples use the free Segmented Control from Wingo UI, the library we build, so we are not neutral; outside facts link to their sources.
What is a segmented control, in web terms?
It is a radio group with a different look. Each segment is one option, exactly one is selected, and selecting one applies at once without a submit button. Apple's Human Interface Guidelines (opens in a new tab) describe it as a way to offer "closely related choices that affect an object, state, or view." Material Design 3 has a close relative, the segmented button (opens in a new tab), so you also see the control searched as a React segmented button.
That framing settles the implementation: the markup should be a radiogroup with radio items, the WAI-ARIA radio group pattern (opens in a new tab) defines the keyboard, and a name should make it submit like native radios. The thumb, the spring and the drag sit on top of that.
How do you add a segmented control in React?
Install it, pass options, and either let it hold its own value or control it:
Each option takes value, label, and optionally icon, badge (a number renders a count that hides at 0), tooltip and disabled. The props that change the look are size (sm, md or lg, a 32, 40 or 48px track), radius, tone (default neutral, a white thumb like iOS that turns a lifted gray in dark mode) or any CSS color, and dividers, the hairlines between unselected segments. Leave defaultValue out and it starts on the first enabled option.
$ npx wingo-ui@latest add segmented-controlIf you came for a React segmented control Tailwind recipe, the whole look is Tailwind v4 utilities on design tokens, and every inner part takes classes through classNames (item, thumb, icon, label, badge, divider). App-wide defaults go through the free UIProvider: <UIProvider defaults={{ "segmented-control": { radius: "full" } }}> rounds every segmented control in the app.
What makes it feel like the iOS control?
Three details: the thumb moves on a spring instead of a linear tween, it follows the finger, and it never steals a scroll.
The thumb animates transform only. It is rendered inside the selected segment, so CSS sizes it at rest. When the value changes, it measures the box it is leaving, jumps there as a transform and springs back to zero offset using the snappy spring from lib/motion (stiffness 500, damping 32). Animating left and width would recalculate layout on every frame; a transform stays on the compositor. Its corner radius is divided by the current scale, so a thumb stretching between a short and a long label keeps round corners. A change mid-flight starts from where the thumb is on screen and keeps its speed.
It answers the finger. Pressing the thumb dips it to 96%. Keep holding and slide sideways and the value follows the segment under your finger, then settles where you let go. The hairlines next to the thumb fade out, like on iOS.
A vertical swipe still scrolls the page. The track sets touch-action: pan-y, so the browser keeps vertical scrolling, and the code waits for 8px of movement before deciding what the gesture is. This is the part to copy if you build your own, trimmed from the component:
touch-action: none on the track would make the drag simpler and trap anyone who starts a scroll on the control. After a drag, the click that follows lands on the segment where the drag began, so the component swallows it; otherwise the value snaps back.
With reduced motion turned on, the thumb jumps to its new place and the press dip is off.
Is a segmented control accessible?
It is when the semantics are a radio group, and that comes almost free from Radix. The Segmented Control is built on @radix-ui/react-radio-group, so the track is a radiogroup, each segment a radio with aria-checked, and the whole control is a single tab stop. Left and Right (Up and Down too) move focus and select, which matches the APG rule for a radio group outside a toolbar. We added Home and End, disabled segments are skipped, and loop={false} stops the arrows at the ends. Mantine takes the same route: its SegmentedControl docs (opens in a new tab) (v9.7.1, October 2026) say it "uses radio inputs under the hood."
Four things are on you:
- A name. Pass
aria-label, oraria-labelledbypointing at a visible heading; both land on the radiogroup. - Text for icons. Give icon options a
labelanyway. WithiconOnly, it becomes the segment'saria-labeland its tooltip, shown on hover, on keyboard focus and on a long press on touch. - A form name. Pass
nameand, inside a<form>, the selected value submits like native radios. - A non-drag path. WCAG 2.5.7 Dragging Movements (opens in a new tab) (level AA) asks that anything done by dragging also work with a single pointer without dragging. Tapping a segment is that path, so keep it. Never make the drag the only way.
One trap if you build on Radix yourself: Radix checks the next radio only if the arrow key is still down when its deferred focus lands, so a very quick press (a screen reader, a remote, automation) can move focus without checking anything. The component selects on any focus that follows an arrow key, so a fast press still changes the value.
How should a segmented control behave on a phone?
It should be 44px tall on touch, share the width, and never flash gray on tap. The md size is a 40px track with a mouse and grows to 44px on coarse pointers (pointer-coarse:h-11). The sm size keeps its 32px look and gets an invisible hit area that reaches 44px; lg is 48px everywhere. The reasoning behind 44px, and the Tailwind variants that apply it only on touch, are in minimum touch target size for React and Tailwind.
fullWidth gives each segment an equal share of the container, the usual layout under a phone header. A segment with a long label keeps its natural width instead of truncating, and the others share what is left. Every segment carries touch-manipulation and a transparent -webkit-tap-highlight-color, so a quick second tap never zooms the page and iOS shows no gray flash. Set dir="rtl" and the layout, the arrow keys and the drag mirror.
The rest of the rules (sheets instead of popovers, safe areas, hover alternatives) are in the pillar on mobile-first React components, and the Mobile UI in React category collects every guide on the topic.
Segmented control vs tabs, a toggle group or radio cards: which one?
Pick by what the choice changes. The look tells you little, since Wingo UI's Tabs have a segmented variant with the same raised thumb. What differs is the semantics, and with them what a screen reader user expects to happen next.
Apple draws a similar line on iOS: "For switching between completely separate sections of an app, use a tab bar instead." Here are the same segments as Tabs, each with its own panel:
$ npx wingo-ui@latest add tabsThe Toggle Group needs one more note. In single mode Radix also renders it as a radiogroup, but its arrow keys only move focus and Space or Enter presses the item. That is the APG rule for a radio group inside a toolbar, where "the button that is checked does not change" while you arrow through. So an alignment switch in an editor toolbar is a single Toggle Group, and a stand-alone time range is a segmented control. When several can be on, the Toggle Group in multiple mode is the only one of the four that fits:
$ npx wingo-ui@latest add toggle-groupRadio cards win when the options are not self-explanatory. "Courier, Locker, Pickup" fits in a segmented control; "Home delivery, FedEx, 1-2 days, $9.99" does not, and squeezing it into a segment hides the price that decides the choice. The React form components guide covers the wider choice between a radio group, a select and a combobox in a form.
What goes wrong with segmented controls?
Four things: too many segments, icons mixed with text, a value outside the options, and a missing name.
- Too many segments. Apple says "no more than about five segments on iPhone", and Flutter's SegmentedButton docs (opens in a new tab) say they are "typically used in cases where there are only 2-5 options." Six or more labels on a 360px screen get cramped or cut off; switch to a select or scrolling tabs.
- Mixing icons and text. Apple's guidelines prefer one or the other in a single control. Pick text, or
iconOnlywith labels as names. - A controlled value that is not in
options. The control shows no thumb, which looks broken. This usually happens when options load after the value. - No name. A radiogroup without
aria-labelreads as an unnamed group of radios.
What does a complete example look like?
An orders screen that filters one list by status. The choice changes the content on screen and there are four short options, so this is the segmented control's job. Counts ride on badges, and a status line tells screen readers how many orders are visible:
In a form, drop the state and give it a name. A server action then reads it like any radio:
Where should you start?
Install the free control and drop it where a time range, a view switch or a unit toggle lives today:
Tabs and Radio Group are free too. The Toggle Group and Nav Tabs from the table above are part of Wingo UI Pro.
FAQ
Does shadcn/ui have a segmented control?
Not as of October 2026. Its component list has Tabs, Toggle Group, Radio Group and Button Group, so building one on shadcn/ui means restyling one of those. Pick the base by what the choice does: Tabs for panels, a radio group for a value.
What is the difference between a segmented control and tabs?
Tabs swap in a different panel and expose tablist, tab and tabpanel roles linked by aria-controls. A segmented control picks a value that changes the content already on screen, such as a time range, and is a radiogroup. Apple's guidelines send switching between separate sections of an app to a tab bar.
How many options should a segmented control have?
Two to five. Apple's Human Interface Guidelines say no more than about five segments on iPhone, and Flutter's Material SegmentedButton docs say segmented buttons are typically used with 2 to 5 options. Past five, use a select or a scrolling row of tabs.
Is a segmented control accessible to screen readers?
It is when it is built as a radiogroup of radio items with a name: one tab stop, arrow keys that move and check, and aria-checked on the selected segment. Icon-only segments also need a text name, which Wingo UI's iconOnly mode takes from each option's label.
Can a segmented control submit with a form?
Yes. Give Wingo UI's Segmented Control a name and, inside a form, it submits the selected value like a radio group, so FormData and server actions read it with formData.get.
Is the Wingo UI Segmented Control free?
Yes. The Segmented Control, Tabs, Radio Group and Select are free and install with npx wingo-ui@latest add. The Toggle Group and Nav Tabs are part of Wingo UI Pro.
- Mobile UI
- React
- Accessibility
- Tailwind CSS
- iOS