Skip to content

Animated Pill Tabs

Segmented pill tabs with a sliding, springy background, ARIA tabs keyboard support and fading panels, shown on an analytics card. Pure CSS and vanilla JS.

Live preview

Open in new tab

Code

Animated pill tabs in pure CSS and vanilla JavaScript: a segmented control in a stone-colored tray where a black pill slides and stretches to the selected tab, with a small spring at the end. The demo switches an analytics card between Today, Week, Month and Year; the same tabs suit pricing periods, settings or product details.

How it works

The root section.pl-tabs contains a .pl-head with the title and the .pl-tablist, followed by one .pl-panel per tab. The tab list holds a decorative span.pl-pill and four button.pl-tab elements. Each panel shows a dl.pl-stats grid of .pl-stat cards with .pl-delta badges and an inline SVG .pl-chart area chart.

The pill is absolutely positioned inside the tray and sits behind the buttons thanks to isolation: isolate and z-index: -1. On DOMContentLoaded the script initializes every .pl-tabs root. placePill() copies the offsetLeft, offsetTop, offsetWidth and offsetHeight of the selected tab into --pl-x, --pl-y, --pl-w and --pl-h, and CSS transitions transform and width with --pl-ease, so tabs of different widths are handled correctly. A ResizeObserver on the tray keeps the pill aligned when the layout changes. Transitions are only switched on after the first position is set (the .pl-ready class); without JavaScript the selected tab simply gets a solid background.

select() updates aria-selected and the roving tabIndex, toggles the hidden attribute on the panels and fires a pl:change event with detail.index and detail.tab. Each panel that becomes visible plays the pl-in fade-up keyframes. On phones the tray spans the full width and the stat cards stack; from 640px the tabs sit next to the title and the stats form three columns.

Customization

  • --pl-pill and --pl-pill-text on .pl-tabs: color of the sliding pill and of the selected tab label.
  • --pl-tray and --pl-muted: tray background and the color of unselected tabs.
  • --pl-ease and --pl-speed: the spring of the slide; cubic-bezier(0.65, 0, 0.35, 1) removes the overshoot.
  • --pl-radius: 999px gives pills; 0.75rem turns the control into rounded rectangles.
  • --pl-accent and --pl-accent-soft: the chart line, chart fill and positive badges.
  • Set aria-selected="true" and tabindex="0" on another .pl-tab (and move hidden to the other panels) to change the tab that is open on load.
  • Listen to pl:change on .pl-tabs to fetch data for the chosen range.

Accessibility

The control follows the WAI-ARIA tabs pattern: role="tablist" with aria-label="Time range", buttons with role="tab", aria-selected and aria-controls, and panels with role="tabpanel" and aria-labelledby. Only the selected tab is in the tab order. Left and Right arrows move between tabs and wrap around, Home and End jump to the first and last tab, and Tab moves into the focusable panel. Change badges include visually hidden "up" or "down" text, and the charts are aria-hidden. Tabs and panels have visible :focus-visible outlines, and prefers-reduced-motion: reduce disables the slide and the panel animation.

Usage notes

For several instances on one page give every tab and panel a unique id; each .pl-tabs keeps its own state.

Requirements

No dependencies. Pure HTML, CSS and JavaScript.

Animated Pill Tabs – Pure CSS tabs component screenshot
Animated Pill Tabs · Pure CSS Tabs

More Tabs

Vertical Tabs – Bootstrap 5 tabs preview

Vertical Tabs

Bootstrap 5 vertical nav-pills tabs styled as a settings panel with a dark sidebar, icons, fading content panes and a scrollable mobile tab strip.

Underline Tabs – Tailwind CSS tabs preview

Underline Tabs

Accessible Tailwind CSS tabs with a sliding underline indicator, arrow-key and Home/End navigation, count badge and scrollable tab bar on mobile.