In this tutorial I will share how I built ScrollSpy with Intersection Observer JS.
I’ve accomplished that with Intersection Observer JS API.
ScrollSpy feature becomes handy on documentation, blog articles, landing pages and other pages with multiple sections.
As a UI designer, I decided to style scrollSpy JS in Neubrutalism UI style.
So, let’s go and see the project in action below ⬇️
Project: ScrollSpy with Intersection Observer JS
Intersection Observer JS: How to Build ScrollSpy
See the Pen ScrollSpy Javascript by jsSecrets (@jssecrets) on CodePen.
Table of Contents
Basic Working Principle
Intersection Observer JS: How to Build ScrollSpy
What is ScrollSpy in a nutshell?
Intersection Observer JS: How to Build ScrollSpy
The “ScrollSpy” term was invented by Bootstrap team.
It is a mechanism that watches Y (vertical) scroll position and updates navigation respectively.
There are different ways to watch the scroll position with JS, but the most modern one is using Intersection Observer JS API.
Working Principle
ScrollSpy Javascript Tutorial
To understand the ScrollSpy working principle you have to understand how Intersection Observer JS API works.
Intersection Observer JS Basics
ScrollSpy Javascript Tutorial
let observer = new IntersectionObserver(callback, options);
It accepts callback function and options object.
options
may look like this:
let options = {
root: document,
rootMargin: "-10% 0px -90% 0px",
threshold: 1.0,
};
Callback function is where work and magic 🪄 is happening.
In our case I check which of the sections is intersecting and the highlight respective nav item.
if (entry.isIntersecting) { ... }
And last, but not least run the observe function.
observer.observe(section)
Core concept
ScrollSpy Javascript Tutorial
Let’s start with core concept of how ScrollSpy Javascript works.
1️⃣ HTML
ScrollSpy JS Tutorial
HTML contains <nav>
with items and number of <section>
elements with the content inside.
2️⃣ CSS
ScrollSpy JS Tutorial
CSS has common styles for the layout and placements.
3️⃣ Javascript
ScrollSpy JS Tutorial
The core of JS script is Intersection Observer.
Code explanation
ScrollSpy JS Tutorial
If you have understood the core concept and working principle of how to ScrollSpy JS is built, the code will be easy to grasp.
HTML
ScrollSpy JS Codepen
<body>
<!-- fixed navigation -->
<nav>
<ul>
<!-- I used data attributes for nav items -->
<li data-section="one" class="active">#1</li>
<li data-section="two">#2</li>
<li data-section="three">#3</li>
<li data-section="four">#4</li>
<li data-section="five">#5</li>
<li data-section="six">#6</li>
</ul>
</nav>
<!--END fixed navigation -->
<!-- sections -->
<section class="one">
<!-- content -->
</section>
<section class="two">
<!-- content -->
</section>
<section class="three">
<!-- content -->
</section>
<section class="four">
<!-- content -->
</section>
<section class="five">
<!-- content -->
</section>
<section class="six">
<!-- content -->
</section>
<!-- END sections -->
</body>
CSS
ScrollSpy JS Codepen
/* basic reset */
@import url("https://fonts.googleapis.com/css2?family=Urbanist:wght@500&display=swap");
*,
::before,
::after {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Poppins", sans-serif;
}
ul {
list-style: none;
}
/* font connection */
@font-face {
font-family: "Gosha Sans";
src: url(../fonts/PPGoshaSans-Regular.otf);
font-weight: 400;
}
@font-face {
font-family: "Gosha Sans";
src: url(../fonts/PPGoshaSans-Bold.otf);
font-weight: 800;
}
* {
font-family: "Gosha Sans";
}
/* nav */
nav {
width: 120px;
background: #fff;
padding: 12px;
border: 6px solid black;
box-shadow: 1px 1px black, 2px 2px black, 3px 3px black, 4px 4px black, 5px 5px black, 6px 6px black, 7px 7px black, 8px 8px black;
height: 320px;
color: black;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
right: 28px;
top: 50%;
transform: translateY(-50%);
z-index: 100;
}
@media (min-width: 1200px) {
nav {
width: 200px;
padding: 24px;
height: 400px;
}
}
nav ul {
width: 100%;
text-align: center;
}
/* nested nav list items */
nav ul li {
display: list-item;
font-size: 16px;
padding: 12px;
transition: 0.4s;
position: relative;
}
@media (min-width: 1200px) {
nav ul li {
font-size: 24px;
padding: 16px;
}
}
/* nav item's active class */
/* active class highlights nav item for user */
nav ul li.active {
background-color: #ff7051;
border: 4px solid #000;
font-weight: 800;
font-size: 24px;
}
@media (min-width: 1200px) {
nav ul li.active {
font-size: 40px;
border: 6px solid #000;
}
}
/* sections */
section {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 48px;
transition: 2s;
flex-direction: column;
}
@media (min-width: 1200px) {
section {
flex-direction: row;
}
}
section .subsection {
width: 100%;
}
@media (min-width: 1200px) {
section .subsection {
width: 50%;
}
}
section img {
max-width: 200px;
}
@media (min-width: 1200px) {
section img {
max-width: 400px;
}
}
section.one {
background-color: #23a094;
display: flex;
}
section.one .subsection {
height: 100%;
}
section.one .subsection:nth-of-type(1) {
background-color: #ffc900;
border: 6px solid #000;
border-right-width: 4px;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 48px;
}
section.one .subsection:nth-of-type(1) .title {
margin-bottom: 24px;
}
section.one .subsection:nth-of-type(1) .paragraph {
max-width: 80%;
}
section.one .subsection:nth-of-type(2) {
background-color: #23a094;
border: 6px solid #000;
border-left-width: 4px;
display: flex;
justify-content: center;
align-items: center;
}
section.one .subsection:nth-of-type(2) img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
section.two {
background-color: #ff90e8;
}
section.three {
background-color: #f1f333;
}
section.three .subsection {
height: 100%;
}
section.three .subsection:nth-of-type(1) {
background-color: #b23386;
border: 6px solid #000;
border-right-width: 4px;
display: flex;
justify-content: center;
align-items: center;
}
section.three .subsection:nth-of-type(1) img {
position: relative;
top: -50px;
}
section.three .subsection:nth-of-type(2) {
background-color: #ff7051;
border: 6px solid #000;
border-left-width: 4px;
display: flex;
justify-content: center;
align-items: center;
}
section.three .subsection:nth-of-type(2) img {
position: relative;
top: 50px;
}
section.four {
background-color: #90a8ed;
}
section.five .subsection {
height: 100%;
}
section.five .subsection:nth-of-type(1) {
background-color: #23a094;
border: 6px solid #000;
border-right-width: 4px;
display: flex;
align-items: center;
padding-left: 48px;
}
section.five .subsection:nth-of-type(1) .paragraph {
max-width: 80%;
}
section.five .subsection:nth-of-type(2) {
background-color: #f1f333;
border: 6px solid #000;
border-left-width: 4px;
display: flex;
justify-content: center;
align-items: center;
}
section.five .subsection:nth-of-type(2) .blob {
width: 700px;
height: 700px;
position: relative;
}
section.five .subsection:nth-of-type(2) .blob svg path {
fill: #ff90e8;
stroke: #000;
stroke-width: 3px;
}
section.five .subsection:nth-of-type(2) .blob img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
section.six {
background-color: #b23386;
}
section.red {
background-color: rgb(233, 81, 81);
color: white;
}
/* text */
.title {
font-size: 24px;
font-weight: 800;
}
@media (min-width: 320px) {
.title {
font-size: 40px;
}
}
@media (min-width: 575px) {
.title {
font-size: 60px;
}
}
@media (min-width: 1200px) {
.title {
font-size: 92px;
}
}
.text {
max-width: 80%;
font-weight: 400;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-hyphens: auto;
-moz-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
font-size: 16px;
}
@media (min-width: 320px) {
.text {
font-size: 24px;
}
}
@media (min-width: 575px) {
.text {
font-size: 32px;
}
}
@media (min-width: 1200px) {
.text {
font-size: 48px;
}
}
Javascript
ScrollSpy JS Codepen
// variables
const sections = document.querySelectorAll('section');
const navElements = document.querySelectorAll('ul li');
let activeSectionIndicator = '';
// intersection observer
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
// if 90% of <section> is visible (intersecting) then
// respective <nav> element highlights
// "90%" is our custom value
// it is set in "rootMargin"
if (entry.isIntersecting) {
// remove highlight from current "active" <nav> item
document.querySelector(`ul li.active`).classList.remove('active');
// get the index of currently intersecting section
activeSectionIndicator = entry.target.classList[0];
// highlight new active <nav> item
document
.querySelector(`ul li[data-section="${activeSectionIndicator}"]`)
.classList.add('active');
}
});
},
// options
{ root: document, rootMargin: '-10% 0px -90% 0px' }
);
// loop for Intersection Observer JS
sections.forEach((section) => {
observer.observe(section);
});
Frequently Asked Questions
ScrollSpy JS Codepen
1️⃣ What is a ScrollSpy?
ScrollSpy is a plugin that automatically updates navigation links or list group components based on the scroll position to indicate which link is currently active in the viewport
2️⃣ How do I add ScrollSpy?
You can learn how to add a ScrollSpy in this detailed tutorial: jssecrets.com/build-scrollspy-javascript-intersection-observer-js
3️⃣ What is the difference between scroll behavior auto and smooth?
scroll-behavior: auto value scrolls instantly, while the
scroll-behavior: smooth value scrolls in a smooth way. The auto value is the default behavior. The smooth value is recommended to provide a better user experience.
4️⃣ What is Intersection Observer API?
Intersection Observer API is a JS API that allows you to asynchronously observe changes in the intersection of an element with the browser’s viewport
5️⃣ What are the benefits of using Intersection Observer API?
Intersection Observer JS API makes it easy to perform tasks that involve detecting the visibility of an element, or the relative visibility of two elements in relation to each other. experience
What to do next?
ScrollSpy Plugin
You can check out my Password Validation project.
Resources
1️⃣ jssecrets.com
2️⃣ Intersection Observer API at MDN
3️⃣ Image from Storyset
4️⃣ Image from Storyset
5️⃣ Image from Storyset