ScrollTo Anchor Nav — Free GSAP ScrollToPlugin Snippet
ScrollTo Anchor Nav · Navigation · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<nav class="san-nav" id="sanNav">
<a href="#san-home" class="san-link is-active">Home</a>
<a href="#san-features" class="san-link">Features</a>
<a href="#san-pricing" class="san-link">Pricing</a>
<a href="#san-faq" class="san-link">FAQ</a>
</nav>
<section class="san-sec" id="san-home" style="--sa:#818cf8"><h2>Home</h2><p>Click the nav — GSAP eases the journey there.</p></section>
<section class="san-sec" id="san-features" style="--sa:#22d3ee"><h2>Features</h2><p>offsetY keeps the sticky nav from covering the headline.</p></section>
<section class="san-sec" id="san-pricing" style="--sa:#c084fc"><h2>Pricing</h2><p>autoKill hands control back the moment you scroll manually.</p></section>
<section class="san-sec" id="san-faq" style="--sa:#4ade80"><h2>FAQ</h2><p>The URL hash still updates — deep links keep working.</p></section>*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:auto}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b0d16;color:#fff}
.san-nav{position:sticky;top:12px;z-index:10;display:flex;gap:4px;width:fit-content;margin:12px auto;padding:6px;border-radius:99px;background:rgba(20,26,46,.9);border:1px solid rgba(255,255,255,.12);backdrop-filter:blur(8px)}
.san-link{padding:9px 18px;border-radius:99px;color:#8a90a8;text-decoration:none;font-size:13.5px;font-weight:600;transition:color .25s,background .25s}
.san-link:hover{color:#fff}
.san-link.is-active{color:#fff;background:#232c4e}
.san-sec{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center;padding:24px;border-top:1px solid rgba(255,255,255,.06)}
.san-sec h2{font-size:clamp(30px,5.6vw,52px);font-weight:800;letter-spacing:-.02em;color:var(--sa)}
.san-sec p{color:#aeb4ca;font-size:15px;max-width:420px;line-height:1.6}gsap.registerPlugin(ScrollToPlugin);
var links = document.querySelectorAll('.san-link');
var NAV_OFFSET = 76;
links.forEach(function (link) {
link.addEventListener('click', function (e) {
e.preventDefault();
var target = link.getAttribute('href');
gsap.to(window, {
duration: 1,
ease: 'power2.inOut',
scrollTo: {
y: target,
offsetY: NAV_OFFSET,
// autoKill stops the tween instantly if the user scrolls,
// instead of fighting them for the scrollbar.
autoKill: true
},
onComplete: function () {
history.replaceState(null, '', target);
}
});
});
});
// Active link tracking: highlight the section nearest the offset line.
var sections = document.querySelectorAll('.san-sec');
window.addEventListener('scroll', function () {
var best = 0, bestDist = Infinity;
sections.forEach(function (sec, i) {
var d = Math.abs(sec.getBoundingClientRect().top - NAV_OFFSET);
if (d < bestDist) { bestDist = d; best = i; }
});
links.forEach(function (l, i) { l.classList.toggle('is-active', i === best); });
}, { passive: true });Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
ScrollTo Anchor Nav — Eased Anchor Scrolling You Actually Control

CSS scroll-behavior: smooth gives you exactly one animation: the browser's fixed curve, no offset, no interruption handling, no completion callback. This sticky pill nav uses GSAP's ScrollToPlugin instead — every anchor click glides with a chosen ease over a chosen duration, lands *below* the sticky nav thanks to offsetY, hands the scrollbar back instantly if the user intervenes, and updates the URL hash on arrival.
scrollTo tweens the scroll position like any property
gsap.to(window, { scrollTo: { y: '#san-pricing' } }) treats the window's scroll position as a tweenable value: pick duration: 1 and power2.inOut and the journey accelerates out and brakes in like every other GSAP motion. Selectors, pixel numbers, 'max', and other elements' positions are all valid y values — and because it's a real tween, it sequences into timelines, can be paused, and reports onComplete.
offsetY solves the sticky-header cover-up
Native anchors scroll the target's top edge to the viewport's top edge — directly underneath any fixed or sticky header. offsetY: 76 lands the section 76px shy, exactly clearing the pill nav's height plus margin. This is the single most common reason teams abandon CSS smooth scrolling (its equivalent, scroll-margin-top, must be maintained on every *target*; offsetY lives once, at the *initiator*).
autoKill is scroll-jacking etiquette
A one-second programmatic scroll is an eternity if the user changes their mind. With autoKill: true, the moment any manual scroll input arrives (wheel, touch, keyboard), the tween kills itself and the user has full control — no rubber-banding, no fighting the scrollbar. Without it, the tween would keep re-asserting its trajectory against the user's input, the cardinal sin of scroll-jacking.
The hash updates without the jump
Clicking calls preventDefault() (or the browser would teleport instantly), then onComplete writes the hash via history.replaceState — so deep links and back-button state stay meaningful, but the write happens *after* arrival and never triggers the native jump. replaceState (vs pushState) keeps intra-page hops from polluting back-button history.
Active tracking by nearest-to-offset-line
The scroll listener highlights whichever section's top is closest to the 76px offset line — the same line scrollTo lands on, so a nav click always ends with its own link active. Nearest-distance beats threshold-crossing logic at the document's ends, where the last section may never reach the line. The listener is passive and does trivial math; for heavier pages, an IntersectionObserver approach like scroll spy nav trades precision for zero per-scroll work.
Note the CSS: scroll-behavior stays auto
If the page sets scroll-behavior: smooth, the browser would *also* smooth GSAP's scroll writes, double-easing every frame into mush. Keeping it auto gives the plugin full authority — a classic integration gotcha.
Customizing it
Tune duration/ease per distance (a common trick: scale duration by pixels traveled), scroll a container instead of the window (gsap.to('#panel', ...)), or pair the x axis for horizontal journeys. Related: observer-highlighted scroll spy nav, the scroll to top button, section decks in observer fullpage, and page-length context from a scroll progress bar.
Step by step
How to Use
- 1Add the GSAP CDNsInclude gsap and ScrollToPlugin from the CDN panel.
- 2Paste HTML, CSS, and JSA sticky pill nav floats over four sections.
- 3Click PricingThe page eases there, landing clear of the nav.
- 4Interrupt a scrollWheel mid-flight — autoKill yields instantly.
- 5Check the URLThe hash updates on arrival, jump-free.
- 6Retune the journeyduration, ease, and offsetY are three numbers.
Real-world uses
Common Use Cases
offsetY, pairing with a table of contents for long guides.scrollTo: 0, with autoKill cancelling the tween the instant a user scrolls manually.replaceState writing the hash without a jump.Got questions?
Frequently Asked Questions
Control: CSS smooth scrolling has one browser-defined curve, no duration choice, no completion callback, no interruption policy, and its offset fix (scroll-margin-top) must be maintained per target. ScrollToPlugin gives you duration, any ease, offsetY at the call site, autoKill for user handoff, onComplete for hash updates, and timeline composability.
Native anchor behavior scrolls the target's top to the viewport's very top — straight under any sticky or fixed header. offsetY: 76 stops the journey 76px short, so headlines land just below the pill nav. One constant at the initiator covers every target, and the active-tracking logic uses the same line so clicks always end self-consistent.
The plugin watches the scroll position each tick; if it ever differs from where the tween just put it — meaning a wheel, touch, or key contributed — it kills the tween immediately. The user gets the scrollbar back mid-flight with no rubber-banding. onAutoKill even lets you react, e.g. clearing a pending active-link change.
The default anchor action teleports instantly — that's why the click is preventDefault-ed. Writing the hash afterward via history.replaceState preserves deep-linking and shareable URLs without triggering the native jump, and replaceState (unlike pushState) keeps five section hops from becoming five back-button steps.
scroll-behavior: smooth must stay off — the browser would re-smooth every frame the tween writes, double-easing into mush (this snippet pins it to auto). ScrollSmoother, by contrast, composes correctly: ScrollToPlugin writes the real scroll position and ScrollSmoother glides content toward it, so the pairing feels natural.
Register the plugin at module scope and fire tweens from click handlers — no mount effect needed for the scrolls themselves; only the active-tracking listener belongs in useEffect/onMounted/ngAfterViewInit with removal in the cleanup. In SPA routing, run the scroll after navigation commits (React useEffect on route change, Vue router afterEach). The pill nav is a natural Tailwind composition of sticky, backdrop-blur, and rounded-full.
Yes. While the ScrollTo Anchor Nav snippet gives you one pattern to copy, the free Responsive Navbar Builder at webdevpuneet.com/navbar-builder/ designs a whole navigation bar: logo, links and dropdown menus, a mobile hamburger menu (dropdown panel, slide-in drawer or full screen) and a scroll effect (sticky, shrink, hide on scroll or transparent). It has six presets, a live preview that runs the real code, and exports HTML + CSS + vanilla JS, React, Tailwind, Vue 3 or Angular — or you can click Fork & Edit to keep editing it in My Code.