Quickly navigate through the dashboard with your keyboard using the command menu.
A pill segmented control with a springy stretch-and-settle sliding indicator — picks slide with a liquid spring that elongates toward the target then contracts into place.
Segmented Control is a pill-shaped toggle — a small row of options like Day / Week / Month — with a bright indicator that slides between picks. Instead of a plain jump, the indicator stretches toward its destination and springs back into place, so it reads like a drop of liquid settling. Reach for it anywhere you'd use tabs or a radio group: time ranges, filters, or a two-or-three-way view switch.
This gem is CSS plus one small vanilla-JS file — no libraries, no build step.
index.html, assets/style.css, assets/script.js, assets/byq-brand.css, assets/gem-frame.css, and the assets/medias/ folder.index.html in any modern browser.There's nothing to compile. The assets/byq-brand.css file (included in the folder) supplies the BYQ brand tokens — colors, fonts, radii — that the gem references, so keep it linked on the page. gem-frame.css only centers the demo and doesn't travel with the snippet. This gem's gem.json lists no GSAP dependency, so there's no CDN to load.
The whole control is one namespaced <section> wrapping a semantic <fieldset> of radio inputs. Real radios do the state-keeping; the sliding pill is just a decorative layer on top.
<section class="byq-gem byq-gem--segmented-control-01">
<fieldset class="byq-gem--segmented-control-01__track">
<legend class="byq-gem--segmented-control-01__legend">Time range</legend>
<div class="byq-gem--segmented-control-01__indicator" aria-hidden="true"></div>
<input class="byq-gem--segmented-control-01__radio" type="radio"
name="segmented-control-01-range" id="sc01-day" value="day" checked />
<label class="byq-gem--segmented-control-01__label" for="sc01-day">Day</label>
<input class="byq-gem--segmented-control-01__radio" type="radio"
name="segmented-control-01-range" id="sc01-week" value="week" />
<label class="byq-gem--segmented-control-01__label" for="sc01-week">Week</label>
<input class="byq-gem--segmented-control-01__radio" type="radio"
name="segmented-control-01-range" id="sc01-month" value="month" />
<label class="byq-gem--segmented-control-01__label" for="sc01-month">Month</label>
</fieldset>
</section>Each option is a hidden __radio paired with a visible __label (linked by for/id). Because they're genuine radios sharing one name, the control is keyboard-accessible and works even before the script runs. The __indicator is the sliding pill; it's aria-hidden since it carries no meaning of its own, and the visually-hidden __legend keeps the <fieldset> semantic for screen readers.
Every selector starts with .byq-gem--segmented-control-01 so the control can't collide with anything else on your page. The track is a relative-positioned flex row, which gives the absolutely-positioned indicator something to anchor to.
.byq-gem--segmented-control-01__track {
position: relative;
display: flex;
padding: 4px;
background: var(--byq-card);
border: 1px solid var(--byq-depth);
border-radius: 999px;
}The indicator is absolutely positioned inside that track. Its left and width start empty because JavaScript measures and sets them — but the animation lives entirely here in CSS:
.byq-gem--segmented-control-01__indicator {
position: absolute;
top: 4px;
bottom: 4px;
left: 0; /* set by JS */
width: 0; /* set by JS */
border-radius: 999px;
background: var(--byq-accent-lime);
transition:
left 0.38s cubic-bezier(0.2, 0.9, 0.3, 1.3),
width 0.38s cubic-bezier(0.2, 0.9, 0.3, 1.3);
}That cubic-bezier(0.2, 0.9, 0.3, 1.3) is the whole trick — the final 1.3 value overshoots past 1, giving the springy overshoot that makes the pill feel liquid. The labels sit above the pill (z-index: 1) and are dimmed by default; the checked one turns dark against the lime fill:
.byq-gem--segmented-control-01__radio:checked
+ .byq-gem--segmented-control-01__label {
color: var(--byq-on-accent, #000);
}A :focus-visible rule draws an outline on the label whose radio has keyboard focus, and a prefers-reduced-motion block strips the transition so the pill just jumps.
The script's only job is measuring where each label sits and feeding those pixel values to the CSS above. It's scoped to the gem root and depends on nothing.
First it grabs the root, the moving parts, and bails out safely if anything is missing:
const root = document.querySelector(".byq-gem--segmented-control-01");
const track = root.querySelector(".byq-gem--segmented-control-01__track");
const indicator = root.querySelector(".byq-gem--segmented-control-01__indicator");
const radios = root.querySelectorAll(".byq-gem--segmented-control-01__radio");A measureLabel helper reads each label's live geometry with getBoundingClientRect() and converts it to an offset relative to the track's inner edge (subtracting the 4px padding):
function measureLabel(label) {
const trackRect = track.getBoundingClientRect();
const labelRect = label.getBoundingClientRect();
const left = labelRect.left - trackRect.left - 4;
return { left, width: labelRect.width };
}The stretch-and-settle happens in slideTo. It first grows the indicator to span both its current spot and the target — instantly, with the transition switched off — so the pill visually elongates:
const stretchLeft = Math.min(currentLeft, target.left);
const stretchRight = Math.max(currentLeft + currentWidth, target.left + target.width);
const stretchWidth = stretchRight - stretchLeft;
setInstant();
indicator.style.left = stretchLeft + "px";
indicator.style.width = stretchWidth + "px";
indicator.offsetWidth; // force reflow to commit the stretched stateReading indicator.offsetWidth forces a synchronous reflow — the browser commits the stretched frame before the next line re-enables the transition. Then it snaps to the real target, and CSS animates the contraction with that overshoot bezier:
setAnimated();
indicator.style.left = target.left + "px";
indicator.style.width = target.width + "px";Finally it wires everything up: each radio's change event calls syncIndicator(true) to animate, a debounced resize listener repositions without animation, and one syncIndicator(false) on load snaps the pill onto the initially-checked option.
radios.forEach((radio) => {
radio.addEventListener("change", () => { syncIndicator(true); });
});
syncIndicator(false); // initial placement — no animation on loadvar(--byq-accent-lime). Swap it to var(--byq-accent-solar) (or another token) in .byq-gem--segmented-control-01__indicator to re-tint it.<input> + <label> pair in the HTML — keep the shared name="segmented-control-01-range" and matching for/id. The script measures whatever labels exist, so no JS edits are needed.cubic-bezier(0.2, 0.9, 0.3, 1.3) on the indicator's transition. Push the last number higher for a bouncier overshoot, or drop it to 1 for a clean glide; change the 0.38s to alter the speed.border-radius: 999px on the track, indicator, and labels makes it fully rounded — lower it (e.g. var(--byq-radius)) for a squarer control.min-width: 72px and padding: 8px 20px on .byq-gem--segmented-control-01__label set each option's width and height.