Sticky Header Navigation with Smooth Scroll
Implement a sticky header navigation with smooth scrolling to anchors, enhancing user experience on long pages.
Code
<!--
Sticky Header Navigation with Smooth Scroll
Smoothly scrolls to in-page anchors while accounting for a sticky header offset, with accessibility and reduced-motion support.
-->
<header id="header" class="sticky">
<nav aria-label="Page sections">
<ul>
<li><a href="#section1">Section 1</a></li>
<li><a href="#section2">Section 2</a></li>
<li><a href="#section3">Section 3</a></li>
</ul>
</nav>
</header>
<style>
.sticky {
position: sticky;
top: 0;
background: white;
padding: 10px 0;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
z-index: 1000;
}
nav ul {
list-style: none;
display: flex;
justify-content: space-around;
gap: 12px;
margin: 0;
padding: 0 12px;
}
nav a {
text-decoration: none;
color: black;
padding: 8px 10px;
border-radius: 8px;
}
nav a:focus-visible {
outline: 2px solid #111;
outline-offset: 2px;
}
</style>
<script>
/**
* Initializes smooth scrolling for in-page nav links.
* Uses event delegation for performance and avoids polluting global scope.
*/
(() => {
const header = document.getElementById('header');
const nav = header?.querySelector('nav');
if (!header || !nav) return;
const prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false;
const getHeaderOffset = () => {
// Read layout once per click; avoids continuous reflows.
const height = header.getBoundingClientRect().height;
return Number.isFinite(height) ? Math.ceil(height) : 0;
};
const isSamePageHashLink = (anchor) => {
try {
const url = new URL(anchor.href, window.location.href);
return url.origin === location.origin && url.pathname === location.pathname && !!url.hash;
} catch {
return false;
}
};
const scrollToTarget = async (target) => {
const offset = getHeaderOffset();
const top = Math.max(0, target.getBoundingClientRect().top + window.scrollY - offset);
// Async boundary keeps this extensible (e.g., analytics) and safe for future awaits.
await Promise.resolve();
window.scrollTo({ top, behavior: prefersReducedMotion ? 'auto' : 'smooth' });
// Keep history in sync without triggering another jump.
if (target.id) history.pushState(null, '', `#${CSS?.escape ? CSS.escape(target.id) : target.id}`);
target.setAttribute('tabindex', '-1'); // Ensure focusable for screen readers
target.focus({ preventScroll: true });
};
nav.addEventListener('click', async (event) => {
const link = event.target.closest?.('a[href^="#"]');
if (!link || !nav.contains(link) || !isSamePageHashLink(link)) return;
event.preventDefault();
try {
const { hash } = new URL(link.href, window.location.href);
const id = decodeURIComponent(hash.slice(1));
const safeId = CSS?.escape ? CSS.escape(id) : id;
const target = document.getElementById(id) || document.querySelector(`[id="${safeId}"]`);
if (!target) return; // Target missing: fail silently (or optionally log)
await scrollToTarget(target);
} catch (err) {
console.error('Smooth scroll navigation error:', err);
}
});
// Example usage: Add <section id="section1">...</section> blocks below the header.
})();
</script>