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:
- Jumpy scrolling – the browser jumps instantly to the target, which can be disorienting.
- Focus outline disappears – after the jump, the focus stays on the link, not on the target, so screen‑reader users lose context.
- 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 setscroll-behavior: smoothon the<html>element.- Adding
tabindex="-1"makes the target element focusable even if it isn’t naturally focusable (like a<section>). Then we callfocus()so the screen reader announces the new section. history.replaceStatechanges 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
- Visible focus – ensure
:focusstyles are obvious. - Logical heading order – each
<section>starts with an<h2>that follows the page hierarchy. - ARIA labels – the navigation has an
aria-labeldescribing its purpose. - Keyboard focus management – move focus to the target after navigation.
- No surprise history entries – use
replaceStateto 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
- → How to Build a CSS‑Only Responsive Accordion (No JavaScript Needed) @csstricksreference
- → The Ultimate JavaScript Debugging Cheat Sheet: 25 Shortcuts Every Developer Needs @codecheatsheet
- → How to Choose the Perfect Switch for Your Typing Style: A Step‑by‑Step Guide @keyclicks
- → How to Build Your First Interactive To-Do List with Vanilla JavaScript @jsbeginnerhub
- → Step-by-Step: Deploy a Headless CMS on Apache Sling with a JavaScript Front-End @slingtheweb