How to make a reading progress bar with React

React

By Xavier Mod on 3rd December, 2020 · 5 min read

TL;DR

A quick tutorial on how to make a simple reading progress bar using React.

Final code at the end of the page!

Reading progress bars have become essential in blog design and development. Users can quicky see how much they've read and know how long until they finish the post. Big players like Medium have popularised the usage of this elements and are actually very simple to use; one React component and we are ready to go!

For this tutorial, I am going to be using React with Hooks and Styled components .

First create a component called ReadingIndicator.js.

Second, we import all necessary dependencies.

js
1import React, { useState, useEffect, useRef } from 'react'
2import styled from 'styled-components'
3/* Install the browser-monads dependency if
4you are using a static site generator like Gatsby.js */
5import { window, document, exists } from 'browser-monads';

Besides functionality, the reading indicator works with two divs. The parent div will serve as a relative wrapper while the second one will be the indicator itself, with an absolute position. The width of the ReadingProgressBar will be changed dynamically from the component's props.

jsx
1const ReadingProgressWrapper = styled.div`
2 position: relative;
3 height: 2px;
4 top: 0;
5 background: rgba(0, 0, 0, 0.2);
6 width: 100%;
7`;
8
9const ReadingProgressBar = styled.div`
10 width: ${props => props.width};
11 background-color: black;
12 position: absolute;
13 z-index: 1;
14 height: 2px;
15`;

Let's now create our core component structure:

jsx
1const ReadingProgress = () => {
2 const [readingProgress, setReadingProgress] = useState(0);
3
4 return (
5 <ReadingProgressWrapper>
6 <ReadingProgressBar width={readingProgress} />
7 </ReadingProgressWrapper>
8 );
9 };
10
11 export default ReadingProgress;

We have set up the structure of our reading indicator. Great. Now let's work on the logic! We just need one small function and our main window scroll listener. Let's do the function first:

js
1const scrollListener = (target) => {
2 var scrollMaxY = window.scrollMaxY || (document.documentElement.scrollHeight - document.documentElement.clientHeight)
3
4 //
5 setReadingProgress(`${Math.floor((window.scrollY || window.scrollTop || document.getElementsByTagName("html")[0].scrollTop) / scrollMaxY * 100)}%`);
6};

Let's break down what we have here:

var scrollMaxY takes the max height of the browser's window. Using just the first property could do the trick, but by adding the second one we make it compatible with other browsers as well.

Then we are updating our state with an output like this one: 40%. Essentially, we are getting the current user's scroll position (cross-browser compatible solution), dividing it by the max height of the window and multiplying it by 100 to get a nice percentage! (Quick maths!)

We're mostly done! Let's create a scroll listener which will execute our brand new function every time the user scrolls the page.

js
1useEffect(() => {
2 window.addEventListener("scroll", scrollListener);
3 return () => window.removeEventListener("scroll", scrollListener);
4});

We've got out function and our listener up and running, so we add them to our component structure and done! This is how the component looks like:

Final code:

jsx
1const ReadingProgressWrapper = styled.div`
2 position: relative;
3 height: 2px;
4 top: 0;
5 background: rgba(0, 0, 0, 0.2);
6 width: 100%;
7`;
8
9const ReadingProgressBar = styled.div`
10 width: ${props => props.width};
11 background-color: black;
12 position: absolute;
13 z-index: 1;
14 height: 2px;
15`;
16
17const ReadingProgress = () => {
18 const [readingProgress, setReadingProgress] = useState(0);
19
20 const scrollListener = (target) => {
21 var scrollMaxY = window.scrollMaxY || (document.documentElement.scrollHeight - document.documentElement.clientHeight)
22
23 setReadingProgress(`${Math.floor((window.scrollY || window.scrollTop || document.getElementsByTagName("html")[0].scrollTop) / scrollMaxY * 100)}%`);
24
25 console.log(readingProgress)
26 };
27
28 useEffect(() => {
29 window.addEventListener("scroll", scrollListener);
30 return () => window.removeEventListener("scroll", scrollListener);
31 });
32
33 return (
34 <ReadingProgressWrapper>
35 <ReadingProgressBar width={readingProgress} />
36 </ReadingProgressWrapper>
37 );
38 };
39
40 export default ReadingProgress;

If you want to see how it looks like, just look up on the top left of the screen. I have implemented it on my own website!

Xavier Mod