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
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.
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'suseState
hook - Apply event listener inside React's
useEffect
hook - Use
lodash.throttle
to reduce make sure the function is called only once perx
seconds - Use a cross browser friendly
getPageHeight()
to get the total height of the page - Explicitly define
0
and100
percent values
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
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.
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.
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.
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.