Anatomy of the Segmented Control
How a tab-like control renders one pill that lives inside whichever tab is active, and lets Framer's layoutId spring it between segments instead of cross-fading.
↓Scroll to step through it
One track, one pill
A segmented control could be built as N buttons each tracking their own "am I selected" background. GodUI builds it as N buttons and exactly one pill, which is only ever rendered inside the currently active tab. Switching segments moves that one element from the old button to the new one.
The track is a role="tablist" with bg-muted, rounded corners, and a
little padding; each option is a role="tab" button. The pill itself is
absolute inset-0 of whichever button is active. It is conditionally
rendered, so it only ever exists in one place in the DOM at a time:
Because the pill is a child of the button, not the track, it's positioned
with plain inset-0. No coordinate math is needed to find the tab it sits
under; the tab's own layout does that for free.
layoutId slide
When value changes, the pill unmounts from the old button and mounts in
the new one. Normally that would be two separate elements popping in and
out. layoutId tells Framer Motion these two mounts are "the same" element:
it measures the old box, measures the new box, and animates a transform
between them instead of letting either one appear:
The per-instance pillSeed counter matters more than it looks: layoutId
is matched globally across the page, so two SegmentedControls rendered at
once must never share an id, or Framer would try to slide a pill between
them. Incrementing a module-level counter on every mount guarantees each
instance gets its own namespace without a prop the consumer has to supply.
There's no separate "pressed" boolean to track. Every tab button carries
active:scale-[0.97], a plain CSS active-state utility rather than a Framer
whileTap, so the dip is instantaneous on both mouse and touch and costs
nothing while the control is idle:
className="... active:scale-[0.97] disabled:pointer-events-none disabled:opacity-40"The result
Two lines of state (value, and a layoutId that only exists to keep
instances apart) plus one conditionally rendered pill. The slide is Framer
measuring two boxes and animating between them; the control tracks no
motion of its own.
- Track
- role="tablist": bg-muted, rounded, p-1
- Segment buttons
- one role="tab" per option
- Sliding pill
- bg-background, absolute inset-0 of the active tab
<div role="tablist" className="inline-flex items-center rounded-lg border bg-muted p-1">{options.map((opt) => { const active = opt.value === value; return ( <button key={opt.value} role="tab" aria-selected={active} onClick={() => select(opt.value)}> {active && ( <motion.span layoutId={layoutId} className="absolute inset-0 rounded-md bg-background shadow-sm" /> )} <span className="relative">{opt.label}</span> </button> );})}</div>Anatomy of the Segmented Control
How a tab-like control renders one pill that lives inside whichever tab is active, and lets Framer's layoutId spring it between segments instead of cross-fading.
- Track
- role="tablist": bg-muted, rounded, p-1
- Segment buttons
- one role="tab" per option
- Sliding pill
- bg-background, absolute inset-0 of the active tab
<div role="tablist" className="inline-flex items-center rounded-lg border bg-muted p-1">{options.map((opt) => { const active = opt.value === value; return ( <button key={opt.value} role="tab" aria-selected={active} onClick={() => select(opt.value)}> {active && ( <motion.span layoutId={layoutId} className="absolute inset-0 rounded-md bg-background shadow-sm" /> )} <span className="relative">{opt.label}</span> </button> );})}</div>One track, one pill
A segmented control could be built as N buttons each tracking their own "am I selected" background. GodUI builds it as N buttons and exactly one pill, which is only ever rendered inside the currently active tab. Switching segments moves that one element from the old button to the new one.
The track is a role="tablist" with bg-muted, rounded corners, and a
little padding; each option is a role="tab" button. The pill itself is
absolute inset-0 of whichever button is active. It is conditionally
rendered, so it only ever exists in one place in the DOM at a time:
Because the pill is a child of the button, not the track, it's positioned
with plain inset-0. No coordinate math is needed to find the tab it sits
under; the tab's own layout does that for free.
layoutId spring: stiffness 520, damping 32
let pillSeed = 0;const layoutId = React.useMemo(() => `segmented-pill-${pillSeed++}`, []);<motion.spanlayoutId={layoutId}transition={{ type: "spring", stiffness: 520, damping: 32 }}className="absolute inset-0 rounded-md bg-background shadow-sm"/>layoutId slide
When value changes, the pill unmounts from the old button and mounts in
the new one. Normally that would be two separate elements popping in and
out. layoutId tells Framer Motion these two mounts are "the same" element:
it measures the old box, measures the new box, and animates a transform
between them instead of letting either one appear:
The per-instance pillSeed counter matters more than it looks: layoutId
is matched globally across the page, so two SegmentedControls rendered at
once must never share an id, or Framer would try to slide a pill between
them. Incrementing a module-level counter on every mount guarantees each
instance gets its own namespace without a prop the consumer has to supply.
There's no separate "pressed" boolean to track. Every tab button carries
active:scale-[0.97], a plain CSS active-state utility rather than a Framer
whileTap, so the dip is instantaneous on both mouse and touch and costs
nothing while the control is idle:
className="... active:scale-[0.97] disabled:pointer-events-none disabled:opacity-40"Viewing week range
The result
Two lines of state (value, and a layoutId that only exists to keep
instances apart) plus one conditionally rendered pill. The slide is Framer
measuring two boxes and animating between them; the control tracks no
motion of its own.
Accessibility
The track is a tablist/tab pair. aria-selected mirrors active
directly off the same boolean that decides whether to render the pill, so
the visual state and the accessible state can never drift apart. Focus is
native <button> focus (focus-visible:ring-2, offset for contrast against
the muted track); there's no roving-tabindex manager, because segmented
controls are typically small enough that browser tab order is enough.
useReducedMotion collapses the pill's spring to { duration: 0 }, so the
pill jumps to the new tab without sliding.
Motion Score
scaleScale spring / pressopacityFade / cross-fade