Sai Yerni Akhil Madabattula
Sai Yerni Akhil's Blog

Sai Yerni Akhil's Blog

Building a Scrollspy and more using React and IntersectionObserver API

Photo by Willian Justen de Vasconcellos on Unsplash

Building a Scrollspy and more using React and IntersectionObserver API

Sai Yerni Akhil Madabattula's photo
Sai Yerni Akhil Madabattula
·Jan 2, 2022·

5 min read

What is an IntersectionObserver ?

An IntersectionObserver is a utility in the native browser APIs which lets us check whether an element has intersected the viewport or any other ancestor.

I came across this while I was trying to build a scroll spy, you might be wondering what a scroll spy is? If you have ever used/worked with Bootstrap, scroll spy implementation is available directly via Bootstrap’s utilities which are built on top of jQuery.

Check out this example and see how it works on Google Docs - okBSKyAI4T.gif

It becomes so easy with bootstrap, but at work, its a React.js project and we aren’t using Bootstrap over there, so I had to search for alternatives and one such library was react-scrollspy but I don’t have the luxury of installing the library in my project plus its deprecated anyways. So I had to roll my sleeves and build one, which serves the purpose. I was as usual googling stuff on how I could and I came across many solutions and one that I found interesting was one by user @Creaforge on Stack Overflow using IntersectionObserver API. I have successfully built that feature and I have gone forward and learnt more about it since I found it very interesting. In this blog post, I’m going to share what I have learnt and also gonna give some examples in which we can use this API.

Here are some examples I have built using IntersectionObserver API -

1. Implementing a ScrollSpy in React

I have written a hook from @Creaforge’s answer on Stack Overflow and took it forward from that. I made use of ref to get the component’s reference to the DOM.

import { useEffect, useMemo, useState } from 'react';

 * @param {ReactRef} ref reference to the DOM
 * @param {object} options (optional)
 * @returns {boolean} if the ref is intersecting the viewport
 *  - The `root` is the scrollable parent of which the ref is a descendant of.
 *  - `rootMargin` is the margin of the root element
 *  - a single `threshold` ranges from 0 to 1.0 (0 - 100%) as also can be an array [0, 0.5,...1.0]
 *  - The callback to the IntersectionObserver constructor is invoked, when the ref is x% of the parent element
import { useEffect, useMemo, useState } from "react";

export default function useOnScreen(ref) {
  const [isIntersecting, setIntersecting] = useState(false);

  let options = {
    root: document.querySelector("#root-el"),
    rootMargin: "0px",
    threshold: 0.5

    // useMemo since the IntersectionObserver does consume more resources
  const observer = useMemo( 
    () =>
      new IntersectionObserver(
        ([entry]) => setIntersecting(entry.isIntersecting),

  useEffect(() => {
    // Removing the observer when the component is unmounted
    return () => {
  }, []);

  return isIntersecting;

Now, this hook will tell me whether an element (ref) is on my screen matching the threshold and lets me set the state.

So now I have a state variable, which contains the latest div which intersected the screen, so I’ll take that state variable and try to highlight the currently active element

const [currentRefOnScreen, setCurrentRefOnScreen] = useState(<id of the ref>);
return  (
      ,index) => (
                        <li className={currentRefOnScreen === index ? 'active' : ''}></li>

The remaining part of the scroll spy left to implement was to scroll to the section when clicked on the nav list item. Since I have access to the ref, I can easily scroll to the ref using ref.scrollIntoView(..)

Here is the link to the final version on

2. Fancy Scroll Animation

There is another example I have tried out to replicate the functionality which I’ve found on the Apple iPhone 13’s product display page.


Here you can see how the battery level changes from 0 to 100 while scrolling. Our target is to replicate that.

Similar to the above scroll spy implementation example we need to observe something (target) within the parent (root)

So now we’ll have to observe the section of the page which contains the battery icon (and maybe the text describing it) and calculate the amount of the targeted element which is visible.

Let us start with the implementation -

  • We’ll start by creating an observer and starting observing the target element

const createObserver = () => {
  let observer;
  let options = {
    root: null,
    rootMargin: "0px",
    threshold: buildThresholdList()

  observer = new IntersectionObserver(handleIntersection, options);

let boxElementToBeObserved;
boxElementToBeObserved = document.querySelector("#app");


We’ll also have a function that gives us the threshold values, here in this case I’m using a list of values because I want the battery percentage to increase from 0 to 1.0 in 10 (numSteps) intervals. Which turns out to be 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7,...1.0. One can increase or decrease the threshold based on the requirements.

Here is how it looks when the steps are reduced to 2 -


and when the number of steps is 10 -


  • Next, we’ll jump into writing our intersection handler, which is called whenever the threshold values are met. calculating the width includes subtracting from 100 because initially 100% of the element will be visible and we need to start from 0 (100 - 100 = 0, Isn’t it?😂)
let prevRatio = 0.0;
const handleIntersection = (entries, observer) => {
  entries.forEach((entry, entryIndex) => { = `${100 - entry.intersectionRatio * 100}%`;
    prevRatio = entry.intersectionRatio; // just preserving for reference incase we need it in future

Entries here are the list of Intersection objects for all the target elements we are observing, in our case, it’ll have only one element.

  • Since we need to change the battery level, I have gotten access to the battery level element to which we have to change the width.
const batteryLevelElement = document.querySelector(".batteryLevel");

Here is the final working demo of the battery animation -

I’m sure there might be better ways of doing without the extensive usage of refs in the implementation of scroll spy and If you have better solutions and any feedback, I’m glad to learn them from you😀. Make use of the comments section for constructive feedback.

References -

  1. IntersectionObserver documentation on MDN -
  2. StackOverflow thread -
Share this