:has() + translate
A segmented control with a sliding indicator
CaveatNever size the indicator in pixels. Hardcode a width and a shift and the pill lines up until someone renames an option — then it sits half over the next label, with no error to tell you. Equal columns and a shift of 100% of itself need no measuring.
FallbackKeep real radios underneath: rebuilt from divs it loses arrow-key navigation and the announced group. Without :has() the pill just stays on the first option and the radios still work.
.seg {
display: inline-grid;
grid-auto-flow: column;
/* equal, whatever the label says */
grid-auto-columns: 1fr;
}
.seg::after {
width: calc((100% - 8px) / 3);
transition: translate .28s;
}
/* one column, measured by the browser */
.seg:has(input:nth-of-type(2):checked)::after {
translate: 100% 0;
}
Published