webdevpuneet.com

Animated Toggle Group — Free HTML CSS JS Segmented Control

Animated Toggle Group · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="tgg-group" id="tggGroup" role="tablist" aria-label="View">
  <span class="tgg-pill" id="tggPill"></span>
  <button type="button" class="tgg-opt is-active" role="tab" aria-selected="true">Day</button>
  <button type="button" class="tgg-opt" role="tab" aria-selected="false">Week</button>
  <button type="button" class="tgg-opt" role="tab" aria-selected="false">Month</button>
  <button type="button" class="tgg-opt" role="tab" aria-selected="false">Year</button>
</div>
<p class="tgg-out" id="tggOut">Showing: <strong>Day</strong></p>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Sliding gradient pill
One element morphs behind the active option.
Springy easing
Overshoot bezier gives a gentle bounce.
Measured positions
offsetLeft and offsetWidth, never hard-coded.
Variable widths
Handles options of different label lengths.
Arrow-key nav
Left/Right move selection, wrapping around.
Tablist ARIA
role=tab and aria-selected on each option.
Resize-safe
Re-measures the pill on viewport change.
Label crossfade
Active text color follows the pill.

About this UI Snippet

Animated Toggle Group — A Sliding-Pill Segmented Control

Screenshot of the Animated Toggle Group snippet rendered live

The animated toggle group is the segmented control where a highlighted pill glides smoothly from one option to the next instead of snapping — the time-range and view switchers you see in dashboards and settings. This snippet builds it with plain HTML, CSS, and vanilla JavaScript, including a springy slide, keyboard support, and proper tab roles.

The sliding pill technique

Rather than moving a background between buttons, a single absolutely-positioned .tgg-pill sits behind all the options at z-index: 0, with the button labels above it. When you select an option, JavaScript measures that button's offsetWidth and offsetLeft and sets the pill's width and translateX to match exactly. Because both properties are transitioned, the pill slides and resizes in one motion to wrap whichever option is active — even when the options have different widths.

Springy easing

The transition uses a custom cubic-bezier(.34, 1.4, .5, 1) whose control point above 1 produces a slight overshoot, so the pill arrives with a gentle spring rather than a flat stop. That tiny bounce is what makes the control feel responsive and physical. The active label colour also crossfades to white so the text contrast follows the pill.

Measuring, not hard-coding

Positions are read from the live layout every time, so the control adapts to any number of options and any label lengths without per-option constants. A resize listener re-measures and repositions the pill so it stays aligned when the viewport — and therefore the button widths — change. This is the key to a segmented control that doesn't drift on responsive layouts.

Keyboard and ARIA

The group uses role="tablist" with each option as a role="tab" carrying aria-selected, and a keydown handler moves the selection with the Left and Right arrow keys, wrapping around the ends. Selecting via keyboard moves focus and the pill together, so the control is fully operable without a mouse and announces its state to assistive tech.

Why a pill behind text beats swapping backgrounds

Animating one shared element is both smoother and simpler than fading a background in and out on each button: there's a single thing to move, it can morph its width, and the transition is continuous across the whole control. The labels never reflow because only the pill's transform and width animate.

Customizing it

Change the gradient, the overshoot in the bezier, or the padding to restyle the control; add or remove options freely and the measurement logic adapts. Wire select() to filter data or switch views. Pair it with a segmented control variant, a chip filter, or a dynamic tabs panel.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA four-option toggle renders with Day active.
  2. 2
    Click an optionThe gradient pill slides and resizes to it.
  3. 3
    Use arrow keysLeft and Right move the selection and pill.
  4. 4
    Watch the outputThe line below updates to the active label.
  5. 5
    Resize the windowThe pill re-aligns to the active option.
  6. 6
    Add an optionDrop in another button — no constants to edit.

Real-world uses

Common Use Cases

Chart time ranges
Switch Day, Week and Month above a line chart widget, with a gradient pill sliding to the chosen option.
Dashboard view switchers
Toggle layouts on a dashboard layout, measuring positions with offsetLeft and offsetWidth so labels of any length work.
Pricing period switches
Pair with a pricing toggle for monthly and annual choices, using an overshooting bezier for a gentle bounce.
Filter category switches
Switch categories beside a chip filter in a listing, with full arrow-key support for keyboard users moving between options.
Settings and tab alternatives
Pick theme or density in a settings panel, or compare with dynamic tabs when content panels change.

Got questions?

Frequently Asked Questions

On each selection JavaScript reads the active button's offsetWidth and offsetLeft from the live layout and sets the pill's width and translateX to match. Because both are measured rather than hard-coded, the pill resizes to fit whichever option is active, even when labels have different lengths.

The transition uses cubic-bezier(.34, 1.4, .5, 1). The second control value above 1 makes the pill overshoot slightly and settle back, producing a gentle spring instead of a flat stop. Both width and transform share this easing so the resize and slide move together.

Yes. A resize listener re-measures the active option and repositions the pill, so it stays aligned when button widths change with the viewport. The same init function runs on load to place the pill under the initially active option.

The group is a role=tablist with each option a role=tab carrying aria-selected. A keydown handler moves the selection with Left and Right arrows, wrapping at the ends, and moves focus and the pill together. So the control is fully operable without a mouse and reports its state to screen readers.

Keep the active index in state and render the buttons from an array. After the active index changes, measure the active button via a ref in a layout effect (useLayoutEffect / watch + nextTick / ngAfterViewChecked) and set the pill's width and transform, so the measurement runs after the DOM updates. The springy CSS ports unchanged.