Updated on Jun 3rd, 20216 min readreactblog

Build A Blog Article Progress Bar With React + Bootstrap

Most of us are familiar with progress bars and probably observe dozens on a daily basis. They are literally a basic building block of the web, and graphic user interfaces in general. Here is an example of the HTML progress element.

Learn more about the HTML progress element at Mozilla

<label for="example">Example progress:</label>
<progress id="example" max="100" value="75"></progress>

Use Case

Progress bars provide feedback to the user, usually the concerning the state of a process happening on their computer or in the cloud. This event could be a file download, installing a new program, transforming a video, or any other process that requires a meaningful period of time to complete. Generally, the progress bar fills with color from left to right, indicating the relative percentage of the task that has been completed.

For the purposes of this article, we are concerned with the user's progress reading a blog post. In a practical sense this value should indicate how far down the page the user has scrolled relative to the total height of the page.

While I wouldn't consider this feature as crucial as a progress bar for a software install, I do think it provides a nice utility to the user and even a slight encouragement to continue reading. I have even seen a bookmark feature built on top of a progress bar - a topic for a more advanced article perhaps.

Setup

This blog is built with React and uses the React Bootstrap component library so they will be the building blocks for our progress bar. Conveniently, Bootstrap has its own Progress element we can use to get started. Check it out here.

The goal is to keep this simple so I will be using Code Sandbox for this project. I used the React template and installed the following dependencies:

  • react-bootstrap@2.0.0-alpha.1
  • bootstrap@5.0.0
  • lodash.throttle@4.1.1
Control Package Version
Control Package Version

It is okay to leave the style import but add the bootstrap components and styles with the following lines above the local style import.

import {useState, useEffect} from 'react'
import Navbar from "react-bootstrap/Navbar";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import ProgressBar from "react-bootstrap/ProgressBar";
import throttle from 'lodash.throttle'

import "bootstrap/dist/css/bootstrap.min.css";
import "./styles.css";

Implementation

Start with a basic Navbar. Add the following code as the return value of the App functional component. Note that we are using the helper classes position-sticky and top-0. These are made available thanks to importing Bootstrap's CSS file.

<div className="App">
   <header className="position-sticky top-0">
    <Navbar bg="dark" variant="dark">
      <Container>
        <Navbar.Brand href="#home">Blog Progress Bar</Navbar.Brand>
      </Container>
    </Navbar>
  </header>
</div>

To simulate a blog article let's us a couple effort saving tricks. The Lorem Ipsum Generator can be used to create as much dummy text as we need. For this project, grab 1 paragraph.

Lorem Ipsum Generator
Lorem Ipsum Generator

Instead of generating 20 paragraphs or going crazy with copy/paste use JavaScript arrays to pump out paragraph text.

Add one paragraph of Lorem Ipsum above App.

const LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae turpis massa sed elementum tempus egestas sed. Tellus cras adipiscing enim eu turpis egestas pretium aenean pharetra. Sed faucibus turpis in eu mi bibendum neque egestas. Consectetur a erat nam at lectus urna duis convallis. Dolor sit amet consectetur adipiscing elit. Neque egestas congue quisque egestas diam in arcu. Lacus luctus accumsan tortor posuere ac ut consequat semper. Malesuada bibendum arcu vitae elementum curabitur vitae nunc sed. In aliquam sem fringilla ut morbi tincidunt augue interdum. Semper feugiat nibh sed pulvinar proin gravida hendrerit.";

I find that separating concerns like this helps make the code easier to follow.

Use the built in Array.prototype.from() to create an array of numbers that can be used to clone as many paragraphs as we need.

Now add the starting point for our progress bar. The React Bootstrap ProgressBar take min, max and now for props. Using 0 and 100 maps conveniently to a percentage. The now value is transformed into the width as a percentage of the colored portion of the bar.

The as prop on Container allows us to swap the underlying HTML elements on components. Here I chose to use main element.

// ...
        </Navbar>
        <ProgressBar min={0} max={100} now={50} />
      </header>
      <Container as="main">
        <Row>
          <Col md={{ span: 6, offset: 3 }}>
            {Array.from(Array(20), (_, i) => i).map((el) => (
              <p key={el}>{LOREM_IPSUM}</p>
            ))}
          </Col>
        </Row>
      </Container>
    </div>
// ...

To track the progress as a percentage we need to know the total height of the blog post and the user's position. To accomplish this an event handler function is needed. In this case we will be listening to the scroll event on the window object and using some match to calculate a percentage.

  • Manage progress with React's useState hook
  • Apply event listener inside React's useEffect hook
  • Use lodash.throttle to reduce make sure the function is called only once per x seconds
  • Use a cross browser friendly getPageHeight() to get the total height of the page
  • Explicitly define 0 and 100 percent values
App.js
import { useState, useEffect } from "react";
import throttle from "lodash.throttle";
import Navbar from "react-bootstrap/Navbar";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import ProgressBar from "react-bootstrap/ProgressBar";

import "bootstrap/dist/css/bootstrap.min.css";
import "./styles.css";

const LOREM_IPSUM =
  "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae turpis massa sed elementum tempus egestas sed. Tellus cras adipiscing enim eu turpis egestas pretium aenean pharetra. Sed faucibus turpis in eu mi bibendum neque egestas. Consectetur a erat nam at lectus urna duis convallis. Dolor sit amet consectetur adipiscing elit. Neque egestas congue quisque egestas diam in arcu. Lacus luctus accumsan tortor posuere ac ut consequat semper. Malesuada bibendum arcu vitae elementum curabitur vitae nunc sed. In aliquam sem fringilla ut morbi tincidunt augue interdum. Semper feugiat nibh sed pulvinar proin gravida hendrerit.";

function getPageHeight() {
  const body = document.body;
  const html = document.documentElement;

  return Math.max(
    body.scrollHeight,
    body.offsetHeight,
    html.scrollHeight,
    html.offsetHeight
  );
}

export default function App() {
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const handleScroll = throttle(() => {
      const { scrollY, innerHeight } = window;
      const pageHeight = getPageHeight();
      setProgress(
        !scrollY
          ? 0
          : scrollY + innerHeight >= pageHeight
          ? 100
          : Math.round(
              ((scrollY + innerHeight * (scrollY / pageHeight)) / pageHeight) *
                100
            )
      );
    }, 100);

    window.addEventListener("scroll", handleScroll);

    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);

  return (
    <div className="App">
      <header className="position-sticky top-0">
        <Navbar bg="dark" variant="dark">
          <Container>
            <Navbar.Brand href="#home">Blog Progress Bar</Navbar.Brand>
          </Container>
        </Navbar>
        <ProgressBar min={0} max={100} now={progress} />
      </header>
      <Container as="main">
        <Row>
          <Col md={{ span: 6, offset: 3 }}>
            {Array.from(Array(20), (_, i) => i).map((el) => (
              <p key={el}>{LOREM_IPSUM}</p>
            ))}
          </Col>
        </Row>
      </Container>
    </div>
  );
}

  • A few style declarations to round out the look of our minimalist example
styles.js
body {
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}

.progress {
  height: 7px;
  border-radius: 0;
}

main {
  padding: 50px 0;
}

p {
  font-size: 1.2rem;
  text-align: justify;
}

A Deeper Examination

Some folks may be happy to copy and paste the code above while others might want to understand the formula on a deeper level. Both are fine, but I thought I would include my this section on my thought process in developing the portion of the code that evaluates the progress value.

The function is concerned with 3 values that are derived from the Document Object Modal (DOM).

Originally, I thought I could get the value for progress by simply dividing scrollY by pageHeight and multiplying by 100. It makes sense, but causes a large chunk of progress at the bottom of the page. The goal to to make each click of the scroll wheel an equal percentage of progress.

One Scroll Click Causes Uneven End
One Scroll Click Causes Uneven End

My first idea to make up for the uneven progress was to account for the innerHeight of the current window. It seemed to me that this value was not being accounted for. However, adding this to scrollY just caused a large chunk of progress at the top of the page.

One Scroll Click Causes Uneven Start
One Scroll Click Causes Uneven Start

Finally, I came up with the concept of spreading this excess value across the entire page. This is why I multiple innerHeight by (scrollY/pageHeight) and then add that value to scrollY. Perhaps there is another approach, but this way ends up producing a nice even progress bar so I am happy with it.

Steady Progress
Steady Progress

Final Thoughts

As is the case with many things, there are libraries out there that can help with a component like this. But now that you have this code, you essentially have the start of your own library to use in project after project. Check out the generic version for a solution that in a little more library agnostic. I still use a couple Bootstrap classes, but the implementation of the progress bar is more exposed.

Check out this project on Code Sandbox

Check out a more generic version w/o React Bootstrap

Benjamin Brooke Avatar
Benjamin Brooke

Hi, I'm Ben. I work as a full stack developer for an eCommerce company. My goal is to share knowledge through my blog and courses. In my free time I enjoy cycling and rock climbing.