Intersection Observer JS: How to Build ScrollSpy

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.

scrollspy javascript
scrollspy javascript

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.

intersection observer js
intersection observer js

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

Note
If you have some Javascript project in mind that you want me to explain, please write it in the comments.
scrollspy js
scrollspy js

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

Ilyas Seisov

Ilyas Seisov

UI/Web designer and Javascript developer with Master's degree in Computer Science. He helps businesses transform ideas into reality with the power of design and code. Ilyas creates modern, aesthetic web applications and reads minds in a free time.

Leave a Reply

Your email address will not be published. Required fields are marked *