Seamless Keyboard‑Friendly Anchor Navigation with CSS and a Little JavaScript

Read this article in clean Markdown format for LLMs and AI context.

A page that lets users jump around with the keyboard feels like a small act of respect. When you press Tab and land on a link, you expect the focus outline to be clear, the scroll to be smooth, and the URL to update without a full reload. In 2024, accessibility is no longer a nice‑to‑have; it’s a baseline. Let’s build a navigation bar that works great for mouse, touch, and keyboard, using only CSS for the visual part and a few lines of JavaScript to keep the URL in sync.

Why the Default Anchor Setup Falls Short

The classic <a href="#section"> works, but it has three quirks that hurt the user experience:

  1. Jumpy scrolling – the browser jumps instantly to the target, which can be disorienting.
  2. Focus outline disappears – after the jump, the focus stays on the link, not on the target, so screen‑reader users lose context.
  3. URL hash changes without history control – each click adds a new entry to the back button stack, making it hard to go back to the previous spot.

We can fix all three without pulling in a heavy library. The trick is to let CSS handle the smooth scroll and visual focus, and let a tiny script update the hash only when the user really wants it.

The CSS Part: Smooth Scroll and Focus Styling

First, enable smooth scrolling for the whole document. This is a one‑liner that works in all modern browsers:

html {
  scroll-behavior: smooth;
}

Next, give the focused link a clear outline. I like a thick, colored outline that matches the brand palette, but you can keep it simple:

nav a:focus {
  outline: 3px solid #0066ff;
  outline-offset: 2px;
}

If you prefer a more subtle look, replace the outline with a background change:

nav a:focus {
  background: rgba(0,102,255,0.1);
}

Both approaches keep the focus visible, which is essential for keyboard users.

Markup: Keep It Semantic

A clean HTML structure makes life easier for screen readers and for us when we add JavaScript later.

<nav aria-label="Page sections">
  <ul>
    <li><a href="#intro">Intro</a></li>
    <li><a href="#features">Features</a></li>
    <li><a href="#demo">Demo</a></li>
    <li><a href="#contact">Contact</a></li>
  </ul>
</nav>

<section id="intro">
  <h2>Intro</h2>
  <p>…</p>
</section>

<section id="features">
  <h2>Features</h2>
  <p>…</p>
</section>

<section id="demo">
  <h2>Demo</h2>
  <p>…</p>
</section>

<section id="contact">
  <h2>Contact</h2>
  <p>…</p>
</section>

Notice the aria-label on the <nav> element. It tells assistive tech that this list is for navigating the page, not a generic menu.

Tiny JavaScript: Keep the URL in Sync Without Overloading History

The goal is simple: when a user clicks a link or presses Enter on a focused link, we scroll smoothly (handled by CSS) and then update the hash without adding a new history entry each time. The history.replaceState method does exactly that.

document.addEventListener('DOMContentLoaded', function () {
  const navLinks = document.querySelectorAll('nav a');

  navLinks.forEach(function (link) {
    link.addEventListener('click', function (e) {
      e.preventDefault(); // stop default jump
      const targetId = this.getAttribute('href').slice(1);
      const targetEl = document.getElementById(targetId);

      if (targetEl) {
        // Scroll to the element; CSS handles the animation
        targetEl.scrollIntoView();

        // Move focus to the target for screen readers
        targetEl.setAttribute('tabindex', '-1');
        targetEl.focus();

        // Update URL hash without creating a new history entry
        history.replaceState(null, '', '#' + targetId);
      }
    });
  });
});

Why This Works

  • e.preventDefault() stops the browser’s instant jump, letting the CSS smooth scroll take over.
  • scrollIntoView() triggers the same smooth behavior because we already set scroll-behavior: smooth on the <html> element.
  • Adding tabindex="-1" makes the target element focusable even if it isn’t naturally focusable (like a <section>). Then we call focus() so the screen reader announces the new section.
  • history.replaceState changes the URL hash without pushing a new entry onto the back‑button stack. Pressing Back will now return you to the previous page, not the previous section.

Keyboard‑Only Users: The Tab Order Matters

When a user tabs through the page, they will land on the navigation links first (because they appear early in the DOM). After clicking, the focus moves to the target section, which is exactly where they expect to be. If you have other interactive elements inside the section, they will be reachable with the next Tab press.

If you prefer to keep focus on the link itself (some designers like that), simply remove the focus() call and the tabindex line. The hash will still update, and the visual outline will stay on the link.

A Quick Accessibility Checklist

  1. Visible focus – ensure :focus styles are obvious.
  2. Logical heading order – each <section> starts with an <h2> that follows the page hierarchy.
  3. ARIA labels – the navigation has an aria-label describing its purpose.
  4. Keyboard focus management – move focus to the target after navigation.
  5. No surprise history entries – use replaceState to keep the back button clean.

Personal Touch: How I Discovered This Pattern

I was polishing a portfolio site for a client when I noticed that the “Projects” link would jump instantly, and the focus stayed on the link. My screen‑reader friend told me it felt like “being dropped into a dark room”. I tried a few things, and the combination of CSS smooth scroll plus a few lines of JS solved it in less than ten minutes. The best part? The code is tiny enough to drop into any project without pulling in a whole router library.

Wrap‑Up: Small Code, Big Impact

You don’t need a heavyweight framework to make anchor navigation keyboard‑friendly. A couple of CSS rules give you smooth scrolling and clear focus, while a handful of JavaScript lines keep the URL tidy and move the focus where it belongs. Add the snippet to your next site, test with Tab and a screen reader, and you’ll see the difference right away.

#accessibility #frontend #ux

Reactions
Do you have any feedback or ideas on how we can improve this page?