Overview
Tab indicators slide an underline (or pill, or notch) under the active tab when the user clicks. Three variants ship: a thin underline that translates X, a filled pill that slides with rounded corners, and a Material-style notch with a dot. The indicator can be driven by :has() in supporting browsers, or by a CSS custom property updated per click in vanilla JS for the rest.
When to use it
Reach for animated tabs on settings panels, profile sections, anything with three-to-six equally-important categories. Skip tabs for more than six categories — nav becomes a scroll surface that collapses the tab metaphor. Skip them for binary toggles (use a switch instead) and for primary navigation (use real links instead so the URL is shareable).
How it works
Tabs render as <button role="tab" aria-selected> with a sibling indicator element. Two implementation paths: the :has() path lets CSS query .tabs:has(button[aria-selected="true"]:nth-child(N)) .indicator to position the indicator without any JS; the vanilla path writes --active-index on a wrapper element on each click, then the indicator uses transform: translateX(calc(var(--active-index) * 100%)) with a transition. Both rely on equal-width tab buttons so the indicator math stays simple; for variable-width tabs measure each button’s offset on click and set an explicit --indicator-x + --indicator-w.
Production gotchas
The :has() selector is supported across all modern engines as of 2023–2024, but if you must support older Firefox, gate the rule behind @supports selector(:has(*)) and fall back to the JS path. Variable-width tabs that re-measure on resize need an ResizeObserver on the tablist or the indicator drifts as the user zooms. The transition must cancel mid-flight if the user clicks rapidly through tabs — without an animation-timing-function that resolves quickly, the indicator overshoots its current target.
Accessibility
Follow the WAI-ARIA tabs pattern: arrow keys move focus between tabs without activating (manual activation pattern) or with activation (automatic activation pattern). Choose automatic only when tab content loads instantly; otherwise users mashing arrow keys trigger expensive renders. Each panel needs role="tabpanel" + aria-labelledby bound to its tab. Under prefers-reduced-motion: reduce drop the indicator transition so it snaps directly to position.
References