Creating a reading progress bar in React
Kevin
Posted on October 11, 2019
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:
const scrollListener = () => {
if (!target.current) {
return;
}
const element = target.current;
const totalHeight = element.clientHeight - element.offsetTop;
const windowScrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
if (windowScrollTop === 0) {
return setReadingProgress(0);
}
if (windowScrollTop > totalHeight) {
return setReadingProgress(100);
}
console.log(windowScrollTop);
setReadingProgress((windowScrollTop / totalHeight) * 100);
};
Will be placed within our ReadingProgress
component.
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:
function App() {
const target = React.createRef();
return (
<>
<ReadingProgress target={target} />
<div className={`post`} ref={target}>post content</div>
</>
);
}
See the docs for further information about createRef
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 liked this post feel free to leave a โค, follow me on Twitter and subscribe to my newsletter. This post was originally published on nehalist.io.
Posted on October 11, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
November 27, 2024