Build a Reading Progress Bar with Next.js and Tailwind

Build a Reading Progress Bar with Next.js and Tailwind

Pete avatar

Published by

Pete

Nowadays I see on almost every blog, news article and just any random website a view progress bar on the top of the page, that shows the reader how much of the article have they read so far.

In this article I’ll show you my take on it using Next.js, Tailwind and Typescript. You can also find the completed Github repo here.


We’ll get started with a new Next.js app. For this you can use either the app router or pages router. I’ll use the app router in this article.

Let’s change our homepage (app/page.tsx) to have the following code:

app/page.tsx
"use client";

import { useRef } from "react";
import { Progressbar } from "./components/Progressbar";

export default function Home() {
  const mainRef = useRef<HTMLElement | null>(null);

  return (
    <main ref={mainRef}>
      <Progressbar target={mainRef} />
      <div className="w-full h-screen bg-blue-200" />
      <div className="w-full h-screen bg-red-200" />
      <div className="w-full h-screen bg-yellow-200" />
      <div className="w-full h-screen bg-green-200" />
    </main>
  );
}

Now, a quick summary of what happens here:

  • We added 4 divs just to extend our page and make it scrollable so we can verify the progress bar works and displays the current read progress.
  • Convert the page to be a client component. We’ll need this because we’ll attach a ref to our main element to use it to determine the scroll depth.
  • Add our ProgressBar on the top. With saying that, let’s jump to that component and create it.

It’s time to create our Progressbar component. Let’s create a new file called Progressbar.tsx and paste this inside:

Progressbar.tsx
"use client";

import { useCallback, useEffect, useState } from "react";

type ProgressbarProps = {
  target: React.RefObject<HTMLElement>;
};

export const Progressbar = ({ target }: ProgressbarProps) => {
  const [readingProgress, setReadingProgress] = useState(0);

  const scrollListener = useCallback(() => {
    if (!target.current) {
      return;
    }

    const element = target.current;
    const totalHeight =
      element.clientHeight - element.offsetTop - window.innerHeight;
    const windowScrollTop =
      window.scrollY ||
      document.documentElement.scrollTop ||
      document.body.scrollTop;

    if (windowScrollTop === 0) {
      return setReadingProgress(0);
    }

    if (windowScrollTop > totalHeight) {
      return setReadingProgress(100);
    }

    setReadingProgress((windowScrollTop / totalHeight) * 100);
  }, [target]);

  useEffect(() => {
    window.addEventListener("scroll", scrollListener);

    return () => window.removeEventListener("scroll", scrollListener);
  }, [scrollListener]);

  return (
    <div className="w-full fixed top-0 left-0 right-0">
      <div
        className="h-2 bg-gradient-to-r from-[#FB7C00] via-[#E73B50] to-[#9E009B]"
        style={{
          width: `${readingProgress}%`,
        }}
      />
    </div>
  );
};

Here’s what happens:

As you can see, we return some JSX that’ll essentially be just a div absolutely positioned to the top of the page. The width we’ll set dynamically based on the readingProgress attribute.

We also have a useEffect that’ll just register a scroll event listener for us. It’s also responsible for removing that event listener once our component unmounts.

And the brain of the component, the scrollListener function. This function uses the parent ref we passed in, and calculates based on the window height how far you’ve scrolled and sets it in the state, causing our component to re-render to make the changes visible.

Usage with React Server Components

Now, to use this component you’ll always have to have the use client directive. But in the case you don’t want to have the parent component also a client component and passing the ref to the Progressbar, you can skip that part.
Instead define an ID/classname on the component/element you want to use as the reference, and in the Progressbar rather than using a ref, you can use document.getElementById to access the parent component. This way you don’t have to have both parent and child as a client component, only the Progressbar.

That’s all for now, let me know if you have any questions. You can find the completed Github repo here.