Navigation✨ Premium

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).

JSNavSmooth

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

Main navigation of a SaaS app — Floating Indicator example 1

① Main navigation of a SaaS app

WhenAt the top of a product page or user dashboard with 3 to 5 main sections (Overview, Features, Pricing) the user switches between frequently.
WhyThe sliding pill signals the active tab without leaving the container — more elegant than a classic underline on a dark background, and the violet glow reinforces brand identity.
Settings--primary: #6366f1, --radius-lg: 0.75rem, 6 px inner padding, 3 items.
Metric tabs in an analytics dashboard — Floating Indicator example 2

② Metric tabs in an analytics dashboard

WhenIn a dashboard with switchable views (Traffic, Conversions, Revenue, Costs) that the user toggles regularly during analysis sessions.
WhyThe smooth slide maintains visual continuity across frequent view changes and reduces disorientation — the animation makes it clear which state is now active.
Settings--primary: #10b981 (brand green), --bg-card: #0f172a, transition .35s cubic-bezier(.44, 0, .56, 1), 4 items.
Category filters on a blog or catalogue — Floating Indicator example 3

③ Category filters on a blog or catalogue

WhenOn a page listing articles or products filtered by category (All, Design, Dev, Marketing) without a page reload.
WhyThe indicator gives immediate visual feedback on the active filter; the simultaneous pill stretch when moving from a short to a long label makes the selection obvious even for closely spaced items.
Settings--radius-lg: 2rem (fully round pill), --primary: #f59e0b, font-size: .85rem, 4 items.

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 add tabindex="0" with a keydown listener on Enter/Space.
  • No semantic roles on the container — add role="tablist" on .floating-nav and role="tab" + aria-selected="true/false" on each item for full screen-reader compliance.
  • Inactive text contrast (#a1a1aa on #12121a): ratio ≈ 7.2:1 — WCAG AA and AAA ✓.
  • Active text contrast (#ffffff on the #6366f1→#8b5cf6 gradient): 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.

Chrome 88+✓ Full
Firefox 87+✓ Full
Safari 14+✓ Full
Edge 88+✓ Full
Mobile iOS✓ Touch OK
Android Chrome✓ Full

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):

index.html — structure
<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>
🔒 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
--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

Positioning relies on 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.
The original code targets the unique IDs 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.
Yes. The current code animates 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().