Create a Smooth Scrolling Anchor for Better Navigation

By · Updated

A lightweight, accessibility-first pattern that makes in-page navigation feel polished—CSS one-liner, offset-safe headings, motion preferences, and a tiny JS enhancer.

Why Smooth Scrolling Improves UX (When Done Right)

Smooth scrolling creates continuity between a click and the destination section. Done poorly, it causes nausea, broken focus, or anchors that hide under fixed headers. Done right, it respects motion preferences, preserves focus for assistive tech, and lands content just below sticky nav.

  • Clarity: Users see where they’re going; fewer “where did I land?” moments.
  • Speed: No dependencies—pure CSS + tiny JS (< 1 KB).
  • Accessibility: Honors prefers-reduced-motion, manages focus, and avoids hidden headings.

Step 1 — Enable Smooth Scrolling in CSS (with Motion Safety)

/* Global smooth scroll */
html { scroll-behavior: smooth; }

/* Respect users who prefer less motion */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

The media query is non-negotiable—never force animation on users who opt out of motion.

Step 2 — Prevent Hidden Headings Under Sticky Nav

Reserve anchor landing space with scroll-margin-top on all jump targets. Set it to your sticky header height + a little breathing room.

:is(h2, h3, h4, [data-anchor]) { scroll-margin-top: 6.5rem; }  /* ≈ 104px */

Using :is() keeps the selector short. Prefer a token (e.g., --header-h) if your header is themable.

Step 3 — Semantic Link + Focusable Target Pattern

Make anchors obvious in markup and friendly for keyboard & screen readers.

<nav aria-label="On this page" class="text-sm space-x-3">
  <a href="#why" class="underline">Why</a>
  <a href="#setup" class="underline">Setup</a>
  <a href="#faq" class="underline">FAQ</a>
</nav>

<section id="why" tabindex="-1">...</section>
<section id="setup" tabindex="-1">...</section>
<section id="faq" tabindex="-1">...</section>

tabindex="-1" lets you programmatically focus the section after scrolling, so screen readers announce it. Don’t use positive tabindex.

Step 4 — Tiny JS Enhancer (Hash Links, Focus, Deep Links)

Intercept same-page hash links, scroll into view (if motion allowed), then move focus to the target. Also support page loads with a hash.

<script>
(() => {
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  function getTarget(hash){
    try { return document.getElementById(decodeURIComponent(hash.replace('#',''))); }
    catch { return null; }
  }

  function scrollToAndFocus(el){
    if (!el) return;
    // Scroll (respect motion preference)
    el.scrollIntoView({ behavior: reduceMotion ? 'auto' : 'smooth', block: 'start' });
    // After scroll settles, shift focus for SRs
    setTimeout(() => { el.focus({ preventScroll: true }); }, reduceMotion ? 0 : 250);
  }

  // Click handler for same-page hash links
  document.addEventListener('click', (e) => {
    const a = e.target.closest('a[href^="#"]');
    if (!a || a.getAttribute('href') === '#') return;
    const el = getTarget(a.getAttribute('href'));
    if (!el) return;
    e.preventDefault();
    history.pushState(null, '', a.getAttribute('href')); // update URL without reload
    scrollToAndFocus(el);
  });

  // Deep link on load
  if (location.hash) {
    const el = getTarget(location.hash);
    if (el) scrollToAndFocus(el);
  }

  // Handle back/forward navigation with hashes
  window.addEventListener('hashchange', () => {
    const el = getTarget(location.hash);
    if (el) scrollToAndFocus(el);
  });
})();
</script>

We use a small delay (≈250ms) before focusing to let smooth scrolling complete. When motion is reduced, focus immediately.

Optional — Auto-Highlight the Current Section in the TOC

An IntersectionObserver can toggle an “active” class on nav links as the reader scrolls, improving orientation on long pages.

<script>
(() => {
  const links = [...document.querySelectorAll('nav[aria-label="On this page"] a[href^="#"]')];
  const map = new Map(links.map(a => [a.getAttribute('href').slice(1), a]));

  const obs = new IntersectionObserver(entries => {
    entries.forEach(({ target, isIntersecting }) => {
      const a = map.get(target.id);
      if (!a) return;
      a.classList.toggle('text-[#d4a856] font-semibold', isIntersecting);
    });
  }, { rootMargin: '-40% 0px -50% 0px', threshold: 0.01 });

  map.forEach((a, id) => {
    const sec = document.getElementById(id);
    if (sec) obs.observe(sec);
  });
})();
</script>

Common Pitfalls (and How to Fix Them)

  • Heading hidden under header: Set scroll-margin-top on targets (Step 2).
  • Screen reader doesn’t announce target: Ensure targets are focusable (tabindex="-1") and focused after scroll.
  • Motion sickness reports: Respect prefers-reduced-motion and shorten durations.
  • Anchor links reload the page in SPA setups: Prevent default and use client-side router or the enhancer above.

QA Checklist

  • ✅ Smooth scrolls when motion is allowed; jumps when reduced motion is on.
  • ✅ Targets land fully visible below sticky header.
  • ✅ Focus moves to the destination, and the section name is announced.
  • ✅ Deep links (/page#section) and back/forward navigation work.
  • ✅ Current section link highlights as you scroll (if enabled).

FAQs

Is CSS alone enough for smooth scrolling?

Yes, for the animation. You still need a tiny script to manage focus and deep-link behavior accessibly.

What if my header height changes on scroll?

Use a CSS variable updated by JS or apply scroll-margin-top to a wrapper that adapts via a class toggle.

Does this work without JavaScript?

Yes—anchors still jump. The JS only improves focus management and URL handling.

Key Takeaways

  • scroll-behavior: smooth; + prefers-reduced-motion = safe animation.
  • Prevent “hidden headings” with scroll-margin-top.
  • Move focus after the scroll so assistive tech announces the destination.
  • Optional TOC highlighting and skip links make long pages feel effortless.
Disclaimer: This tutorial is provided for educational and informational purposes only and does not constitute legal, financial, or professional advice. All content is offered “as-is” without warranties of any kind. Readers are solely responsible for implementation and must ensure compliance with applicable laws, accessibility standards, and platform terms. Always apply the information only within authorized, ethical, and legal contexts.

Spot an error or a better angle? Tell me and I’ll update the piece. I’ll credit you by name—or keep it anonymous if you prefer. Accuracy > ego.

Portrait of Mason Goulding

Mason Goulding · Founder, Maelstrom Web Services

Builder of fast, hand-coded static sites with SEO baked in. Stack: Eleventy · Vanilla JS · Netlify · Figma

With 10 years of writing expertise and currently pursuing advanced studies in computer science and mathematics, Mason blends human behavior insights with technical execution. His Master’s research at CSU–Sacramento examined how COVID-19 shaped social interactions in academic spaces — see his thesis on Relational Interactions in Digital Spaces During the COVID-19 Pandemic . He applies his unique background and skills to create successful builds for California SMBs.

Every build follows Google’s E-E-A-T standards: scalable, accessible, and future-proof.