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.
Setup
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:
"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.
Progressbar component
It’s time to create our Progressbar
component. Let’s create a new file called Progressbar.tsx
and paste this inside:
"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>
);
};
Quick breakdown of the component:
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.
Top comments (0)