How to create a reading progress bar with React and Styled Components

Whenever i am reading a blog post or an article online, I find the user experience improved when i see a progress bar at top of the page indicating how much content has been read and how much is left to read. Since I’m launching my own blog, I wanted to write a blog post describing how I’ve implemented this component with React and Styled Components. You should see the component in action by looking at the top of this page in the blog post found on my site kasperdue.com I’ve deployed the component to NPM with the name read-progressbar, so feel free to use it and comment on how i can improve it! Step 1: Track scroll position and extract the height and the top values on our target container. The amount of progress shown in the progress bar will be determined by the amount of scrolling we have done inside the container we want to track. My post is wrapped in a container which contains the height value and the top value we will need in order to calculate how much of the content we have read.

useEffect(() => {
    if (props.attachTo && props.attachTo.current && props.attachTo.current.getBoundingClientRect()) {
        window.addEventListener('scroll', trackScrollEvent);
    }
    return () => {
        window.removeEventListener('scroll', trackScrollEvent);
    };
}, [props.attachTo]);

I’ve created a component called ReadProgressBar where you will pass a ref to the component that you want to track the progress. Whenever the ref is passed, i attach trackScrollEvent function to the scroll event on the window object.

The trackScrollEvent function will calculate the amount of scrolling done on the screen with the containers position as a context. We will use the getBoundingClientRect property of an element which will return CSS border boxes associated with the element. This will be useful, because we need the top and height value to calculate the progress.

const trackScrollEvent = () => {
  const { top, height } = props.attachTo.current.getBoundingClientRect();
  const progress = (top / (height - window.innerHeight)) * 100;
  const translateValue = progress < -100 ? 0 : -100 - progress;
  setProgress(translateValue < -100 ? -100 : top > 0 ? 0 : translateValue);
};

We will then take the innerHeight of the window object to determine when the end of the container has been reached. If we are scrolling and the top of the container is off screen at the top, then the top value will have a negative value, and then we know if the progress should occur. We then subtract the window height from the container height and divide it with the top value and multiply it all with 100 to get a percentage value. Step 2: Pass progress to component and animate the translateX property in style attribute The Bar component will start with a width of 100% and a translateX value of -100%. We do not want to animate the width value due to performance issues, so instead we will animate the translateX value and slide it in from the side based on the progress. We will also hide the progress bar if the target container is not in the view yet.

<ReadProgressBarContainer color={props.backgroundColor} hide={progress === -100 || progress === 0}>
  <Bar color={props.color} style={{ transform: `translateX(${progress}%)` }} />
</ReadProgressBarContainer>

To get a smooth animation when the progress is increased or decreased we will apply transition: .1s ease to the style properties, and we will apply will-change: transform; property to tell the browser which property we are going to animate. That is all! We can now import this component into our sites and pass a ref to it and see the magic happen in the top of our pages. I’ve extended the component so we can pass a color property to style the progress bar, and a background color property to change the background of the container. You can find the source code for the component on Github here. Feel free to comment or send a message on how i can improve the code, or a different approach to improve the overall quality.