Sliding tab pill
Segmented control where the pill measures and slides to the selection
"use client";
import { useEffect, useId, useRef } from "react";
/**
* Segmented tab bar with a pill that slides to the selected label.
*
* State is radio inputs, not React state, so the control works before hydration
* and stays keyboard-navigable for free. The effect only measures the checked
* label and drives the pill; it never owns which tab is selected.
*/
export function TabPillBar({
tabs,
onSelect,
}: {
tabs: { id: string; label: string }[];
onSelect?: (id: string) => void;
}) {
const pillRef = useRef<HTMLSpanElement>(null);
const name = useId();
useEffect(() => {
const pill = pillRef.current;
const bar = pill?.parentElement;
if (!pill || !bar) return;
const move = (animate: boolean) => {
const selected = bar.querySelector<HTMLElement>("input:checked + [data-tab]");
if (!selected) return;
// Kill the transition for un-animated moves (mount, resize), otherwise the
// pill visibly flies in from 0,0 on first paint.
if (!animate) {
pill.style.transition = "none";
pill.getBoundingClientRect();
}
pill.style.transform = `translate(${selected.offsetLeft}px, ${selected.offsetTop}px)`;
pill.style.width = `${selected.offsetWidth}px`;
pill.style.height = `${selected.offsetHeight}px`;
if (!animate) {
pill.getBoundingClientRect();
pill.style.transition = "";
}
};
move(false);
// Signals CSS that the pill is measured and can take over the selected state.
bar.dataset.pill = "on";
const onChange = (e: Event) => {
move(true);
const target = e.target as HTMLInputElement;
if (target?.value) onSelect?.(target.value);
};
bar.addEventListener("change", onChange);
const observer = new ResizeObserver(() => move(false));
observer.observe(bar);
return () => {
bar.removeEventListener("change", onChange);
observer.disconnect();
delete bar.dataset.pill;
};
}, [onSelect]);
return (
<div className="t-tabs">
<span ref={pillRef} className="t-tabs-pill" aria-hidden />
{tabs.map((tab, i) => (
<div key={tab.id} className="contents">
<input
type="radio"
name={name}
id={`${name}-${tab.id}`}
value={tab.id}
defaultChecked={i === 0}
className="peer sr-only"
/>
{/* `peer`, not `has-[:checked]` — the input is a sibling of the label,
not a descendant, so has-* never matches and the selected label
stays grey on the white pill. */}
<label
htmlFor={`${name}-${tab.id}`}
data-tab={tab.id}
className="flex min-h-[44px] cursor-pointer items-center rounded-iq-core border border-transparent px-4 text-iq-small text-iq-ink-muted transition-colors duration-200 hover:text-iq-ink peer-checked:text-iq-canvas sm:min-h-0 sm:whitespace-nowrap sm:rounded-full sm:py-2"
>
{tab.label}
</label>
</div>
))}
</div>
);
}