
Smooth Scrolling Sticky ScrollSpy Navigation
Smooth Scrolling Sticky ScrollSpy Navigation êŽë š
Yesterday evening I was working on a documentation page. The page layout is quite classic, as it consists of a content pane on the left and a sidebar navigation on the right. Looking for a way to make the page less dull I decided to add a few small things to it:
- Smooth Scrolling when clicking internal links
- A Sticky Navigation, so that the sidebar navigation always stays in view
- A âScrollSpyâ to update the active state of the navigation
These three additions make the page more delightful, and best of all is: theyâre really easy to implement!
In this post Iâll lay out the details.
The result
First things first, hereâs a recording of the end result so that you get an idea of what Iâm talking about:
The Markup
The markup is really basic:
- A
mainelement surrounds our contentdivandnav. - Each piece of content is wrapped in a
sectionwhich gets anidattribute. The sidebar navigation then links to itsid
<main>
<div>
<h1>Smooth Scrolling Sticky ScrollSpy Navigation</h1>
<section id="introduction">
<h2>Introduction</h2>
<p>âŠ</p>
</section>
<section id="request-response">
<h2>Request & Response</h2>
<p>âŠ</p>
</section>
<section id="authentication">
<h2>Authentication</h2>
<p>âŠ</p>
</section>
âŠ
<section id="filters">
<h2>Filters</h2>
<p>âŠ</p>
</section>
</div>
<nav class="section-nav">
<ol>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#request-response">Request & Response</a></li>
<li><a href="#authentication">Authentication</a></li>
âŠ
<li class=""><a href="#filters">Filters</a></li>
</ol>
</nav>
</main>
Sprinkle some CSS on top to lay everything out â using CSS Grid here â and you have a fully working â albeit dull â page:
1. Smooth Scrolling
Enabling smooth scrolling is really easy, it you can enable it using a single line of CSS:
html {
scroll-behavior: smooth;
}
đ± Yes, thatâs it!
In the demo embedded below, click any of the links in the nav and see how smooth it scrolls:
For browsers that donât support this you could add this JS fallback:
// Smooth scrolling for browsers that don't support CSS smooth scrolling
if (window.getComputedStyle(document.documentElement).scrollBehavior !== 'smooth') {
document.querySelectorAll('a[href^="#"]').forEach(internalLink => {
const targetElement = document.querySelector(internalLink.getAttribute('href'));
if (targetElement) {
internalLink.addEventListener('click', (e) => {
targetElement.scrollIntoView({
behavior: 'smooth',
});
e.preventDefault();
});
}
});
}
However, browserâs that donât support scroll-behavior: smooth; also donât support behavior: "smooth" for Element#scrollIntoView, so thereâs not real advantage to adding this JS fallback.
2. Sticky Navigation
To make the navigation stay in place as you scroll we can rely on position: sticky;. As with Smooth Scrolling, this is a really simple CSS addition:
main > nav {
position: sticky;
top: 2rem;
align-self: start;
}
đââïž
Since weâre using CSS Grid to lay out the children of <main>, adding align-self: start; to <nav> is an important one here. If we would omit it, the nav element would be as high as the enclosing main element. If that were the case, then nav would never be able to stick.
In the demo embedded below, click any of the links in the nav and see how the nav now also stays in view while the rest of the page scrolls:
3. ScrollSpy with IntersectionObserver
Update 2021.07.19
Thanks to CSS @scroll-timeline we can now also implement a ScrollSpy using only CSS! See my post up on CSS-Tricks to get the details.
Thanks to the almighty IntersectionObserver we can implement a ScrollSpy. Basically we use it to watch all section["id"] elements. If they are intersecting, we add the .active class to any link that links to it. For styling purposes we donât add .active to the link itself, but to its surrounding li element.
In code, that becomes this little snippet:
window.addEventListener('DOMContentLoaded', () => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.getAttribute('id');
if (entry.intersectionRatio > 0) {
document.querySelector(`nav li a[href="#${id}"]`).parentElement.classList.add('active');
} else {
document.querySelector(`nav li a[href="#${id}"]`).parentElement.classList.remove('active');
}
});
});
// Track all sections that have an `id` applied
document.querySelectorAll('section[id]').forEach((section) => {
observer.observe(section);
});
});
đĄ
To make the transition to and from .active not too abrupt, add a little blob of CSS to ease things:
.section-nav a {
transition: all 100ms ease-in-out;
}
Complete Demo
Putting everything together, we end up with this:
Delightful, no? đ
đĄ
If youâre also looking for more inspiration to make your interfaces more delightful, be sure to check Hakim El Hattabâs âBuilding Better Interfacesâ talk. Recommended stuff!