Floating Indicator
A sliding pill indicator highlights the active tab — position and width are measured in real time via getBoundingClientRect() and interpolated by a cubic-bezier(0.44, 0, 0.56, 1).
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



How it works
The indicator (.floating-nav-indicator) is a <div> with position: absolute inside the .floating-nav container (which is position: relative). It appears visually behind labels because items carry z-index: 1. Its background is a 135° linear gradient from #6366f1 to #8b5cf6, plus a violet box-shadow glow at 40% opacity. On load, JS positions it under the first tab immediately, without a visible transition.
On click, the effect calls getBoundingClientRect() on the clicked item and the nav container, then computes a relative offset by subtracting the two left values. It writes style.width and style.left on the indicator. The measurement is taken in real time on every click, making the effect insensitive to label width variations — a short-to-long transition produces a simultaneous stretch-and-slide.
CSS handles the animation via transition: .4s cubic-bezier(.44, 0, .56, 1) on .floating-nav-indicator. This curve is a marginally sharper ease-in-out than the native ease-in-out preset (cubic-bezier(.42, 0, .58, 1)), giving the pill a crisp sense of weight without lingering. Both animated properties (left and width) trigger layout on every frame — for GPU-only animation they can be replaced by transform: translateX() + scaleX().
Accessibility
- prefers-reduced-motion: not handled in this code — the CSS transition remains active even when the OS requests reduced motion. Minimal fix:
@media (prefers-reduced-motion: reduce) { .floating-nav-indicator { transition: none } }. - Items are
<span>elements, not natively focusable — the nav is not reachable by keyboard. For WCAG 2.1.1 compliance, replace them with<button>elements or addtabindex="0"with akeydownlistener on Enter/Space. - No semantic roles on the container — add
role="tablist"on.floating-navandrole="tab"+aria-selected="true/false"on each item for full screen-reader compliance. - Inactive text contrast (
#a1a1aaon#12121a): ratio ≈ 7.2:1 — WCAG AA and AAA ✓. - Active text contrast (
#ffffffon the#6366f1→#8b5cf6gradient): ratio ≈ 4.3:1 — slightly below the AA threshold of 4.5:1 for standard-size normal text.
Browser compatibility
Uses getBoundingClientRect() and CSS transitions — available in all modern browsers without polyfills.
Without JS, the indicator stays at its initial HTML-defined position and no error is thrown — the nav stays readable but static.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="floating-nav" id="floatingNav">
<div class="floating-nav-indicator" id="floatingIndicator"></div>
<span class="floating-nav-item active" data-index="0">Tab 1</span>
<span class="floating-nav-item" data-index="1">Tab 2</span>
<span class="floating-nav-item" data-index="2">Tab 3</span>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| --primary | #6366f1 | Start color of the pill gradient. Replace with your brand color — the end color (#8b5cf6) is hard-coded in the .floating-nav-indicator CSS. |
| --bg-card | #12121a | Background of the floating nav container. |
| --text-secondary | #a1a1aa | Color of inactive item labels. Ensure a ≥ 4.5:1 ratio against your background. |
| --radius-lg | 0.75rem | Border-radius for both the nav container and the pill. Set to 2rem for a fully round pill. |
| --border | rgba(255,255,255,0.08) | Border of the floating container — adjust opacity to hide or accentuate the frame. |
| transition (.floating-nav-indicator) | .4s cubic-bezier(.44, 0, .56, 1) | Duration and curve of the slide. Switch to .25s ease for a snappier feel, or .6s for a wider, slower sweep. |
| padding (.floating-nav) | 6px | Inner padding between the container border and the items — controls the gap around the pill. |
| box-shadow (.floating-nav-indicator) | rgba(99,102,241,.4) 0 4px 15px | Glow halo under the pill. Change the color and blur to match your palette. |
FAQ
getBoundingClientRect(), which reads coordinates at click time. If the window resizes afterwards, no recalculation is triggered. Add window.addEventListener('resize', () => m(activeItem)) where activeItem is the currently .active item — or use a ResizeObserver on the container for finer-grained reactivity.floatingNav and floatingIndicator inside an IIFE. To support multiple instances, extract the logic into a reusable function: function initFloatingNav(navEl, indicatorEl) { const items = navEl.querySelectorAll('.floating-nav-item'); /* … */ } and call it for each pair, passing elements directly instead of IDs.left and width, both layout-triggering properties. For a compositor-only animation, replace the indicator with a full-width element and animate transform: translateX(offsetPx) scaleX(ratio) with transform-origin: left — the browser composites the transform without touching layout. The offset and ratio still need to be computed in JS via getBoundingClientRect().