Overview
Segmented control built on real <input type="radio"> with a pill indicator that slides via translateX driven by a --active-index CSS custom property updated on change. Native radios drive the :checked state; the pill is purely decorative.
When to use it
Reach for segmented controls for two-to-four mutually exclusive options where all options should be visible at once — time-range pickers, view-mode switchers, tab replacements where the content below stays in place. Skip them for more than four options (use tabs) and for one-shot choices where the state does not persist visually.
How it works
Render N <input type="radio"> elements sharing the same name attribute, each with a matching <label>. Hide the inputs visually with opacity: 0 + absolute positioning, then style the labels as the visible segments. On each input change event, write --active-index: {N} on the wrapper element. A pill indicator (a single absolutely-positioned element under the labels) uses transform: translateX(calc(var(--active-index) * 100%)) with a transition to slide between positions. The pill is purely decorative; the radio inputs drive everything else.
Production gotchas
Mixing label widths (e.g. “Day” vs “Month”) breaks the 100% multiplier — either pad labels to equal width with flex: 1 or measure each label and write explicit --pill-x + --pill-w values on change. The pill must sit underneath the label text via z-index so click targets remain on the labels. Make sure labels are cursor: pointer and have enough padding to be comfortable touch targets — segmented controls crammed under 44px tall fail mobile usability.
Accessibility
Native radio inputs give arrow-key navigation and aria-checked announcement for free. Wrap them in a <fieldset> + <legend> describing the choice (the legend can be visually hidden with .sr-only styles). Under prefers-reduced-motion: reduce drop the pill transition so it snaps to the active segment. Verify focus ring visibility on the hidden input transfers visually to the label via :focus-visible on the input + sibling selector to the label.
References
Implementation depth
The reliable version starts with real radio or tab semantics, then lets the sliding pill become a visual confirmation of state. CSS custom properties should move the indicator; the checked control should still be the source of truth.
Avoid building this as buttons with only click handlers. Keyboard users expect arrow-key movement inside a radio group or tablist, visible focus on the active option, and a state that is announced before the animated pill catches up.