Navigation✨ Premium

Pill Indicator

Animated pill indicator that slides to the active tab with a springy bounce — position computed at runtime via getBoundingClientRect, unlimited items.

JSTab

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Navigation has 36 effects, including 4 free. Explore the category →

3 usage examples

Primary navigation — SaaS dashboard — Pill Indicator example 1

① Primary navigation — SaaS dashboard

WhenThe interface exposes several sections (Home, Analytics, Settings) reachable from a single horizontal rail at the top of the page.
WhyThe sliding pill makes the active section immediately obvious and confirms each click with a micro-bounce — far more expressive than a simple color change or underline.
Settings3–5 items, .pill-indicator background matched to brand color, transition .25s for snappy feedback, --bg-card slightly darker than the page background.
Period filter — Analytics view — Pill Indicator example 2

② Period filter — Analytics view

WhenA table or chart exposes multiple time granularities (Day / Week / Month / Year) the user toggles frequently without navigating away.
WhyThe pill provides instant visual feedback on the selected period; its bounce distinguishes a click from a hover and eliminates ambiguity about the active state.
Settings4 short items, reduced padding (0.4rem 1rem) for compactness, --radius-full: 8px for a rectangular tab look, transition .2s ease-out without bounce for a data-dense UI.
Pricing plan toggle — Monthly / Annual — Pill Indicator example 3

③ Pricing plan toggle — Monthly / Annual

WhenA pricing section offers two billing cycles; the pill toggle switches price display without reloading the page.
WhyThe elastic slide draws the eye to the chosen option and gives the feeling of a physical control — the back-out overshoot makes the selection tangible and memorable.
Settings2 items only, .pill-indicator background in emerald green (#10b981) to signal savings, each item taking ~50% of the container width.

How it works

The indicator (.pill-indicator) is an absolutely-positioned <div> inside the relative .pill-nav container. On each click, the script reads getBoundingClientRect() of the target item and the container, computes the left offset and copies the width, then assigns both values in px to the indicator's inline style. The CSS transition takes over and animates the movement.

The transition uses cubic-bezier(0.175, 0.885, 0.32, 1.275) over .3s — a back-out easing that produces a slight overshoot before settling, creating the bounce effect without any JS spring physics. Swapping it for ease-out or none removes the bounce entirely.

.pill-link spans carry position: relative; z-index: 1 to stay visually above the sliding indicator. The active item receives the active class (color #fff); inactive items stay in --text-secondary. The script initializes the indicator on the first item at load time via i.length > 0 && l(i[0]).

Accessibility

  • prefers-reduced-motion not handled: the code has no motion preference check. Add @media (prefers-reduced-motion: reduce) { .pill-indicator { transition: none } } to your stylesheet for motion-sensitive users.
  • .pill-link items are <span> elements — not keyboard-focusable by default. For full accessibility, add role="tab", tabindex="0", and a keydown handler (Enter/Space) to each item.
  • .pill-nav carries no role="tablist" in the base code — add it if the indicator controls panel content along with aria-selected and aria-controls attributes.
  • Contrast: active text (white #fff) on the pill background (#6366f1) reaches ~4.5:1 — borderline WCAG AA for normal text. Inactive text (#a1a1aa on #12121a) exceeds 7:1 — AAA.
  • No aria-label is set on the nav container in the base code — add one to match your use case (e.g. aria-label="Main navigation").

Browser compatibility

Uses getBoundingClientRect, addEventListener, and CSS transitions — supported in all modern browsers since 2019. Zero external dependencies.

Chrome 88+✓ Full
Firefox 78+✓ Full
Safari 13+✓ Full
Edge 88+✓ Full
Mobile iOS✓ Touch OK
Android Chrome✓ Full

If <code>getElementById('pillNav')</code> finds no element, the <code>if (e && n)</code> guard prevents any fatal JS error; items remain clickable but without the animated indicator.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="pill-nav" id="pillNav">
  <div class="pill-indicator" id="pillIndicator"></div>
  <span class="pill-link active" data-index="0">Home</span>
  <span class="pill-link" data-index="1">About</span>
  <span class="pill-link" data-index="2">Work</span>
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
background (.pill-indicator) #6366f1 Pill background color — hardcoded in the CSS, replace the value directly with your brand color.
--bg-card #12121a Background of the .pill-nav container.
--radius-full 9999px Rounding of the pill and container. 6px gives a rounded rectangular tab look.
--text-secondary #a1a1aa Text color of inactive items.
transition (.pill-indicator) .3s cubic-bezier(.175,.885,.32,1.275) Slide duration and easing. Use none to remove all animation, ease-out for a bounceless slide.
padding (.pill-link) 0.5rem 1.5rem Inner spacing of each item. Reduce to 0.4rem 1rem for a compact nav.
(extra item) 3 items Add as many spans as needed — getBoundingClientRect recomputes the exact position on every click, no hardcoded index.

FAQ

Yes. The script measures each item's position via getBoundingClientRect at click time — no index is hardcoded. Add or remove <span class="pill-link"> elements freely; the indicator follows automatically.
The line i.length > 0 && l(i[0]) initializes on the first item. Replace i[0] with the desired index (e.g. i[2]), or use e.querySelector('.pill-link.active') to read the active state defined in the HTML.
By default the JS uses getElementById('pillNav') — one element per page. For multiple instances, give each a unique id and duplicate the logic block for each nav/indicator pair, or iterate over document.querySelectorAll('.pill-nav') and reference the indicator via DOM relation (nav.querySelector('.pill-indicator')).