WebCraft

: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.

CSS
.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

Questions

<details name>

Can the sliding indicator work without JavaScript?

Yes. Keep real radios under the labels and let :has() read which one is checked — the pill is moved by a rule, not by a click handler measuring anything.

Why does my indicator drift out of line with the labels?

Because it was sized in pixels. Hardcode a width and a shift and it lines up until someone renames an option, then sits half over the next label with no error anywhere. Equal columns and a shift of 100% of the pill itself need no measuring at all.

Why radios rather than divs with a class?

Arrow-key navigation, the announced group, and the value arriving in a form submission all come free with radios and have to be rebuilt by hand without them. Without :has() support the pill simply stays on the first option and the radios still work.