UIDex

Segmented Control

الشريط المقسّم

role="radiogroup"

also calledsegmented button، segmented picker، toggle group، joined button group، UISegmentedControl، pill switcher، سيجمنتد كنترول، المتحكّم المقسّم، مفتاح مقسّم، أزرار ملتحمة، شريط اختيار مضغوط، تبويبات مصغّرة

A segmented control is a row of options joined inside one frame, where exactly one is chosen and the selection background moves to whichever option you press. Its job is to pick a value, so its semantics are the semantics of a radio group: role="radiogroup" on the container, role="radio" with aria-checked on each segment. It is not tabs. A tab carries role="tab" and aria-selected and points with aria-controls at a panel that appears and disappears with it, while a segmented control often has no panel under it at all, and its whole effect may be re-sorting a table already on screen or changing the date range of a chart. A toolbar is a different thing again: its buttons are independent actions, none of them holds a selected state, and nothing stops the user pressing two in the same second. Nor is it a button group, where shape is all that joins the buttons and each one does something different; the segments here are mutually exclusive values of one field that knows which of them is current. The test that settles it in review: if a full panel of content with its own heading sits underneath, those are tabs, and if the result is a single value read from a variable or submitted with a form, that is a segmented control.

If you called it…

"the buttons stuck together where one stays highlighted""little tabs inside a rounded box""the monthly or yearly toggle on a pricing page""the pill that slides under whichever option you pick""the iPhone style picker with two or three choices"

Live specimen

Interact with the demo. Every part is real and numbered.

نطاق التقرير

يعرض آخر ٧ أيامname="range" value="week"
RTLأول خيار عند اليمين، والسهم الأيسر هو «التالي»، جرّبه بلوحة المفاتيح

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a segmented control that picks a time range from four options. Make the container an element with role="radiogroup" and an accessible name through aria-labelledby, and put one <input type="radio"> per segment inside it, all sharing the same name value, hidden by clipping (sr-only) rather than display: none, so the single Tab stop and arrow-key selection come from the browser with no JavaScript. Wrap each input in a <label for> that fills its whole cell. Add one keydown listener that reads getComputedStyle(el).direction and swaps ArrowLeft with ArrowRight under rtl, calling preventDefault, because WebKit does not make that swap for native radios. Split the width with grid-template-columns: repeat(4, minmax(0, 1fr)) so no segment changes width when the selection changes. Put one capsule behind the selected segment and move it with transform alone: either anchor it with physical left: 0 and measure with offsetLeft, or anchor it with inset-inline-start: 0 and use translateX(calc(var(--i) * 100% * var(--dir))) with --dir set to -1 under [dir="rtl"]. Do not mix the two approaches. Drop the capsule transition under prefers-reduced-motion: reduce without hiding it. Draw a one pixel divider between each pair of unselected segments and hide it with opacity or a transparent colour instead of removing the border. Round the two ends of the strip with border-start-start-radius and border-end-start-radius on the first segment and the matching end properties on the last. Use logical properties for all padding and margins (padding-inline, margin-inline), and write left or right nowhere except the capsule anchor if you took the physical route. Add a focus ring on the segment driven by :focus-visible on the hidden input, and keep it above the capsule in z order. Hold the number of segments between two and five.

How it behaves right-to-left

This section is ours alone.

Segment order

mirrors

DOM order does not change; the inline axis is what flips, so the first option lands at the right end of the strip in Arabic and the last at the left. A flex or grid row does this by itself, so do not add flex-direction: row-reverse or an order property under [dir="rtl"]. The second flip puts the visual order back where it started and leaves what the eye sees disagreeing with where Tab goes. The one thing left to mirror by hand is a glyph that draws a direction of its own: text alignment icons are stacked lines flush to one side, align-to-start sits flush right in Arabic, so flip those glyphs with transform: scaleX(-1) under [dir="rtl"] or ship a second SVG.

Moving the indicator

mirrors

transform is a physical property with no logical counterpart anywhere in CSS. The common bug is anchoring the capsule with inset-inline-start: 0 and then moving it with translateX(calc(var(--i) * 100%)): the anchor moved to the right edge of the strip in Arabic while the translation still pushes rightward, so the capsule leaves the strip on the first selection. Sign the translation instead: translateX(calc(var(--i) * 100% * var(--dir))) with --dir set to -1 under [dir="rtl"]. The alternative that needs no variable is to anchor with physical left: 0 and measure the segment with offsetLeft, so both halves are physical. Mixing one logical property with one physical one is what breaks it.

The end corners

mirrors

The strip is rounded at its two ends only, and the middle segments have square corners. rounded-l and rounded-r, meaning border-top-left-radius and its siblings, pin the rounding to the physical left and right, so in Arabic the first segment comes out square on the outside and rounded on the inside. Use border-start-start-radius and border-end-start-radius on the first segment and border-start-end-radius and border-end-end-radius on the last: the first word names the block axis, the second the inline axis, and in Tailwind they are rounded-s-* and rounded-e-*. :first-child remains the correct selector, because it follows DOM order and DOM order is logical-start order.

The arrow keys

mirrors

In a radio group an arrow moves focus and changes the value in the same keystroke, and in Arabic the key that means next is ArrowLeft. Do not count on the engine to swap the two for you: Chromium reads the computed direction of the input and flips ArrowLeft and ArrowRight under rtl, while WebKit still treats ArrowRight as next whatever the direction is, so the same markup steps backwards in Safari. Read the direction with getComputedStyle(el).direction inside a keydown listener and swap the keys there with preventDefault, whether you drive the group yourself or patch native radio inputs. ArrowUp and ArrowDown never swap, because the block axis does not flip for Arabic.

Latin labels

never mirrors

The labels on this control are short, and many are pure Latin: PNG and SVG, 1D and 1W, USD. A Latin word on its own renders correctly, but a neutral character next to it takes the paragraph direction and jumps to the other end of the label, so +20% inside an Arabic strip appears on screen as 20%+. Isolate each label on its own with dir="ltr" on its element or with <bdi>, and do not fix it by putting dir="ltr" on the whole strip, which reverses the order of the segments along with it.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIArole="radiogroup" + role="radio"The pair that makes the strip a value picker rather than tabs; the state lives in aria-checked, not aria-selected.
HTML<input type="radio" name="range">The shared name alone gives you the single Tab stop and arrow-key selection with no script; the RTL arrow swap is the part engines disagree on.
Ant Design<Segmented options={[…]} />Ant Design, Mantine and Radix Themes all ship it under this name; Ant Design builds each segment over a hidden radio input inside a label.
Material UI<ToggleButtonGroup exclusive>Without exclusive the buttons stay independently pressable and the single-choice meaning is gone.
Radix UI<RadioGroup.Root orientation="horizontal" dir="rtl">The role and the arrow navigation come ready, and dir is what swaps ArrowLeft and ArrowRight; the joined look is still your CSS.
SwiftUIPicker(…).pickerStyle(.segmented)The same control is UISegmentedControl in UIKit and NSSegmentedControl in AppKit, and Apple files it under pickers rather than tabs.
CSSborder-start-start-radiusThe logical corner that rounds the correct end of the strip once the page flips.

See also

Updated · 2026-08-19