Reading progress bars, like the one you can find on my blog at the top of single posts, are a nice little addition to give detailed information about how far the reader has progressed on the current post. The scrolling bar is not really meaningful in this regard; it includes your entire page, which means that your header, comments, footer, etc. are part of the indication.
Creating a reading progress bar which tells you the actual progress of just the current post content in React is quite easy - especially with hooks, which make our component even smaller.
The ReadingProgress component
Our ReadingProgress
component will do the following things:
- make use of the
useState
hook which will be responsible for reading and setting our reading progress - make use of the
useEffect
hook which will be responsible for handling the scroll event and properly update our progress bar on scroll - return the reading progress bar with the proper width
So let's dive right into the implementation:
const ReadingProgress = ({ target }) => {
const [readingProgress, setReadingProgress] = useState(0);
return <div className={`reading-progress-bar`} style={{width: `${readingProgress}%` }} />
};
This is the foundation for our component. readingProgress
will be used as width (in percent) for our progress bar. The only prop for our component is target
, which will be a reference to our DOM container of the post - more on that in a few moments.
First let's implement our listener, which will update our progress bar on scroll events:
windowScrollTop
tries a bunch of different values which fixes undefined
values for some browsers (e.g. Safari).
There's one problem with this implementation: 100% reading progress is only achieved if we've scrolled past our target. That's pretty unlikely to be true (except you scroll one line after finished reading one line, which would make you really weird) - so we need to slightly adjust how our reading progress is calculated:
const totalHeight = element.clientHeight - element.offsetTop - window.innerHeight;
This should yield to a more accurate result in terms of when the bar should show finished.
Next we'll put our listener into a useEffect
hook, which makes our entire component to look like this:
const ReadingProgress = ({ target }) => {
const [readingProgress, setReadingProgress] = useState(0);
const scrollListener = () => {
if (!target.current) {
return;
}
const element = target.current;
const totalHeight = element.clientHeight - element.offsetTop - (window.innerHeight);
const windowScrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
if (windowScrollTop === 0) {
return setReadingProgress(0);
}
if (windowScrollTop > totalHeight) {
return setReadingProgress(100);
}
setReadingProgress((windowScrollTop / totalHeight) * 100);
};
useEffect(() => {
window.addEventListener("scroll", scrollListener);
return () => window.removeEventListener("scroll", scrollListener);
});
return <div className={`reading-progress-bar`} style={{width: `${readingProgress}%`}} />;
};
The returned function from our useEffect
hook is basically just what happens when the component is unmounted (see Effects with Cleanup in the docs).
Last but not least we need to use our component somewhere. At this point we'll need create a ref on our target container and simply pass this to our ReadingProgress
component:
Now your reading progress bar should work perfectly fine - except that you can't see it because it has no height. Fix this by adding some CSS:
.reading-progress-bar {
position: sticky;
height: 5px;
top: 0;
background-color: #ff0000;
}
Done! Now your readers no longer get lost in the sheer unending length of your posts and always know when it'll be over.
To see a fully working example you can take a look at this code pen:
Third party packages
There are some third party packages out there which handle this exact problem. As far as I've found out most of them are outdated and/or no longer maintained - but what's even more relevant at this point: do you really need a third party dependency for a really simple component with around 30 lines of code? Well, honestly, I don't think so.
Conclusion
As you've seen implementing a reading progress bar in React is pretty easy. Thanks to hooks we can implement this component as a very small function component with little to no overhead.
If you've got any improvements regarding the implementation or think this could be done better please let me know in the comments!