DEV Community

Noah Glushien
Noah Glushien

Posted on

How to Create a Reading Position Indicator in Vue

Have you ever seen those reading progress indicators that exist in some online articles? You know the ones that indicate how much of the article you've scrolled. They are often displayed by stretching across the width of the page toward the top of the browser window.

Have you ever wondered how to create one? Well, if you're anything like me, then you've tried building one yourself without doing any research. Then after a few hours of failed attempts, you realize that you have no idea what you're doing and there is no way your feeble code will ever work. Then after you feel utterly defeated you decide to do some Googling.

That's when you learn about an HTML element you had no idea existed. Introducing the <progress></progress> tag and its corresponding value and max attributes.

The following is my Vue implementation after finding the solution I was looking for on CSS-Tricks. The JavaScript on that article was written in jQuery, so if you're not using jQuery in your project you may need to find another solution. That was the position I was in so maybe you'll find that my solution will also work for you.

First Things First

The first thing we need to do is to place the <progress></progress> tag somewhere in our HTML. I'm not building this out as a Vue component right now although I may do that in the future. For this article, it's okay to just place the tag wherever you want the progress bar to appear in the DOM. If you're using Vue-CLI to build your project scaffolding then you can do all of this directly in the App.vue file.

If you chose to install Vue Router while configuring your project in Vue-CLI, you may need to use the Home.vue file instead.

When adding the tag to the DOM you'll also want to add an initial value attribute, and you'll want to assign that a value of 0. Since we're writing this in Vue I decided to add that property to the data object and I called it progBarValue.

So far my code looks like this (I added a ref attribute for easy referencing later on):

<progress :value="progBarValue" ref="prog"></progress>
data() {
  return {
    progBarValue: 0
  }
}

Setting the Max Value

Since the max value needs to be calculated based on the height of the document we're going to scroll, this is where we need to use JavaScript. To do this, I'm going to create another data property to store the value of the height I'm going to calculate. This will allow me to get the value using a method and then use that value as a computed property. If this sounds confusing hopefully looking at my next code example will help clarify.

For now, I'll just add the new property, clientHeight, to my data object and give it an initial value of null.

Next, to calculate the height of the scrollable page we just need to subtract the height of the document from the height of the window. But how do we get the height of either of them and where should you place this logic? Here is how I'm doing it.

The first thing I'm doing is getting the height of the document and I'm writing my logic directly within the mounted() lifecycle hook. Then I'm assigning that height as the value of my data property.

mounted() {
  this.clientHeight = document.clientHeight;
  // you may need to use document.body.clientHeight
}

Then I'm creating a computed method, progBarMax, to do the subtraction and to also assign the max attribute on the progress tag.

computed: {
  progBarmax() {
    return this.clientHeight - window.innerHeight;
  }
}

Now, my updated progress HTML tag looks like this.

<progress :value="progBarValue" :max="progBarMax" ref="prog"></progress>

Updating the Value Attribute on Scroll

Now we have a progress tag in the DOM with value and max attributes. So far so good. Next, we need to find a way of updating the value attribute as we scroll down the page. To do that I'm creating a method that I'm calling updateProgressValue. Then I'll call that method using an event handler later. Be careful not to make the same mistake I did and use an arrow function to create your method or you're going to stare at the console log message stating it can't find the property of undefined wondering what on Earth you could have done wrong.

methods: {
  updateProgressValue: function() {
    this.progBarValue = window.pageYOffset;
  }
}

All I have to do now is call my new method using the scroll event handler. I'm also going to do this directly within the mounted() lifecycle hook. I'm sure an argument can be made to put this somewhere else, but this works for me and the purposes of this article.

My updated code looks like this.

mounted() {
  window.addEventListener("scroll", this.updateProgressValue);
  this.clientHeight = document.clientHeight;
}

What About the CSS?

Last but not least we need to style the progress indicator. This CSS was not written by me, it came directly from the source article over on CSS-Tricks as referenced earlier in this article.

progress {
  /* Positioning */
  position: fixed;
  left: 0;
  top: 0;

  /* Dimensions */
  width: 100%;
  height: 5px;

  /* Reset the appearance */
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none;

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

  /* Progress bar container for Firefox/IE10+ */
  background-color: transparent;

  /* Progress bar value for IE10+ */
  color: red;
}

progress::-webkit-progress-bar {
  background-color: transparent;
}

progress::-webkit-progress-value {
  background-color: red;
}

progress::-moz-progress-bar {
  background-color: red;
}

And that's all there is to it. Hopefully, you found this little tutorial useful and maybe it cleared up some confusion you had around Vue. If you've found any issues with my logic I apologize. I modified this solution to track the height of a specific block of text on my page and to not show/track the reading progress until the user has scrolled to that part of the page. I didn't want to overly complicate this demo so the logic I wrote above is what I remember starting with before all of my specific changes.

If you have any recommendations for a more efficient implementation then I'm all ears.

Top comments (0)