Dynamic Reading Progress Bar


I'm sure fjall-rs isn't the first tech blog to implement a dynamic reading progress bar, but it's the first the caught my eye. Implementing it was trivially easy, though there are some browser pitfalls to be aware of. Here it is, in full:

// Animate the progress bar
let progressBar = document.querySelector(".progress-bar");
if (progressBar) {
 let html = document.querySelector("html");
 let header = document.querySelector("header");
 const updateProgressBar = () => {
  // This is the only way I found to deal with the mobile address bar
  let effectiveScrollHeight = html.scrollHeight - window.innerHeight;
  // Only enable this when the post is big enough
  if (effectiveScrollHeight > 200) {
   let newHeight = Math.round(
    100 * (html.scrollTop / effectiveScrollHeight)
   );
   progressBar.style.width = `${newHeight}%`;
  } else {
   progressBar.style.width = "0";
  }
 };

 document.addEventListener("scroll", () => {
  updateProgressBar();
 });
 // Scroll events don't always fire when the window resizes
 document.addEventListener("resize", () => {
  updateProgressBar();
 });
}
.progress-bar {
  position: fixed;
  left: 0;
  top: 0;
  height: 3px;
  background: #5ca3ff;
  /* so you can click through */
  pointer-events: none;
  /* raise above everything else */
  z-index: 100;
  /* make the transition nice and smooth */
  transition: width 1s ease-in-out;
}

And I think the result is quite pleasing:

Update

I realized that with the previous solution the progress bar would sometimes scroll off the screen due to mobile overscrolling. Plus, I now realize that its current placement makes it look like its indicating a page load, which might be misleading.

Since we already have a fixed header, let's go ahead and put the progress bar right below it. Technically this doesn't totally fix overscrolling, but in practice users don't overscroll that much, and if they do then I'm happy to leave this behavior as a lackluster easter egg.

We could just put the current div-based solution inside the header, but let's flex our little grey cells and try for something a bit more interesting. Can we accomplish our task without creating another html element at all? This isn't rhetorical, really think about it. I'll wait.

If you said "let's animate the bottom border using the newly-minted CSS @property rule", then wow, I mean good job. I guess. Whatever. I thought it was cool. Anyway, this is what that looks like:

:root {
  --split: 0%;
}

@property --split {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

.app-header {
  border-image: linear-gradient(
    to right,
    var(--accent) var(--split),
    transparent var(--split)
  ) 1;
  transition: --split 0.4s ease-in-out;
  border-bottom: 3px solid;
}

The @property isn't strictly necessary, but if we use it then the browser can animate the value we set JavaScript.

header.style.setProperty("--split", `${width}%`);

I think this looks even better:

And I love that this gave me an excuse to include a video of a page on that same page. Just delightful.