Make a reading progress bar for your blog

Make a reading progress bar for your blog

Can we add anything to a standard blog that would enhance the reading experience?

How about a reading progress bar?

The progress bar

The progress bar is sticky and only appears when the post comes into view. Scroll down and you will see a funky purple bar as you go. ๐Ÿ’œ

See the Pen Reading Progress for Blog by Rob (@robjoeol) on CodePen.


<progress id="reading-progress" max="100" value="0"></progress>

I chose to use progress element because this is the semantic HTML match for the job, swipe right! โœ…

We use the following attributes:


It is not trivial to style progress, you need to do a bit of extra work to use it, instead of reaching for a div as most people do! ๐Ÿ™„๐Ÿ˜„ You can read this article to get the finer details on that.

We want the progress bar to stick to the top of the post, so we use the properties: position: sticky; and top: 0;. We use all the browser prefixes to avoid any compatibility hiccups.

For the styling of the bar itself, I have clarified what is what by using CSS variables, as you can see you need to cater to 3 different Browser groups for consistent styling, using different properties for the same outcome. It looks good in Firefox and Chrome for sure, I havenโ€™t checked it in other Browsers.

:root {
--progress-width: 100%;
--progress-height: 8px;
--progress-bar-color: rgb(115, 0, 209);
--progress-bg: none;
--progress-border-radius: 5px;

progress {
position: -moz-sticky;
position: -ms-sticky;
position: -o-sticky;
position: -webkit-sticky;
position: sticky;
top: 0;

/*Target this for applying styles*/
progress[value] {
/* Reset the default appearance */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;

/* Get rid of default border in Firefox. */
border: none;

width: var(--progress-width);
height: var(--progress-height);

/* Firefox: any style applied here applies to the container. */
background-color: var(--progress-bg);
border-radius: var(--progress-border-radius);

/* For IE10 */
color: var(--progress-bar-color);

/* For Firefox: progress bar */
progress[value]::-moz-progress-bar {
background-color: var(--progress-bar-color);
border-radius: var(--progress-border-radius);

/* WebKit/Blink browsers:
-webkit-progress-bar is to style the container */

progress[value]::-webkit-progress-bar {
background-color: var(--progress-bg);
border-radius: var(--progress-border-radius);

/*-webkit-progress-value is to style the progress bar.*/
progress[value]::-webkit-progress-value {
background-color: var(--progress-bar-color);
border-radius: var(--progress-border-radius);


The JavaScript is quite straightforward, and hopefully is self-explanatory! ๐Ÿ˜…

I use an Intersection Observer, which tells us when the post is in view. We use this to ensure that we only update the progress bar when it is in view. This API is very well-supported by Browsers now.

To find out what is our current position in the post, we check the top coordinate of its bounding box. If it is negative, then we have scrolled into (or past) our post by some amount, we take this value and divide it by the height of the bounding box to get the percentage scrolled.

The last piece is to add a scroll listener for the page (window), which calls our function to update the progress bar.

const post = document.getElementById("post");
const progress = document.getElementById("reading-progress");
let inViewport = false;

let observer = new IntersectionObserver(handler);


//Whenever the post comes in or out of view, this handler is invoked.
function handler(entries, observer) {
for (entry of entries) {
if (entry.isIntersecting) {
inViewport = true;
} else {
inViewport = false;

// Get the percentage scrolled of an element. It returns zero if its not in view.
function getScrollProgress(el) {
let coords = el.getBoundingClientRect();
let height = coords.height;
let progressPercentage = 0;

if (inViewport && < 0) {
progressPercentage = (Math.abs( / height) * 100;

return progressPercentage;

function showReadingProgress() {
progress.setAttribute("value", getScrollProgress(post));

//scroll event listener
window.onscroll = showReadingProgress;

Optimize the Code

The performance of our code is fine, but can be improved. If you are interested, read on!

There are 2 parts of our code that make it perform sub-optimally.

The first part is that some methods trigger the Browser to recalculate the layout (known as reflow in Mozillaโ€™s terminology). This is an expensive operation and should be done only when necessary. When we call getBoundingClientRect(), we trigger this.

The second part is that scroll events can fire at a high rate. If the event handler is executed at this rate, it can be wasteful.

So, what can we change?

Only trigger layout when necessary

We can change our logic a bit so that getBoundingClientRect() is only called when the post is in the viewport.

optimize code by moving getBoundingClientRect

Optimize the event handler

We want to limit how often the scroll event handler is called to update the progress bar.

Debouncing regulates the rate at which a function is executed over time, and is a common optimization technique.

We have a few options:

  1. You can use libraries that have a debounce function such as Lodash and Underscore.
  2. You can use the requestAnimationFrame callback.
  3. You can make your own debounce implementation.

The recommendation is to use requestAnimationFrame if you are โ€œpaintingโ€ or animating properties directly. We are changing the value property, which triggers painting, so we will go with it.

The advantage we gain with requestAnimationFrame is that the Browser executes changes the next time a page paint is requested, whereas with a debounce function it executes at a pre-determined rate that we pick.

The code change is quite small.

var timeout;

window.onscroll = function () {
if (timeout) {

timeout = window.requestAnimationFrame(function () {

I recommend this article if you would like to learn more about debouncing and requestAnimationFrame.

Whatโ€™s the performance gain?

I compared the performance for a fast scroll through the article from top to bottom. Here are the results from Chrome Devtools. You can see in the optimized code, the painting time is about 75% less (highlighted).

performance comparison in chrome devtools

Browser support

Support is widespread for requestAnimationFrame, it works in all Browsers from IE10 and later.

Final Words

Thanks for reading! If you enjoyed the post, let me know.

Happy hacking! ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป๐Ÿ™Œ