Sometimes we need a way to show users how much they have read from an article. And the best way to do this is by showing a progress bar that shows this in percentage.
To achieve this, we have to know how much they have scrolled through that article (or anything), then use this to calculate the completed percentage. We also need a component to display that percentage to the user.
If you’re interested in learning how to build this, check out the following demo on CodePen, then keep reading to see how to build it.
Step 1: Create App.vue
We’re going to use Vue CLI 3’s Instant Prototyping to run this project. So create App.vue
, and put the following into the <template>
section:
<template>
<div id="app">
<div class="card">
<progress-bar :value="progress" />
<div class="text-section">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Cupiditate
architecto voluptatum, laboriosam quisquam quod minus molestias. Rem ut
quidem, corrupti nihil molestiae deserunt, iusto velit unde atque
mollitia, eum ad maiores exercitationem. Soluta harum sit cupiditate
eos, commodi itaque nihil, beatae dolorem ducimus, repudiandae, vero quo
corporis sed laborum at maxime dicta dolores perferendis! Possimus
repellat velit iste quod recusandae suscipit vitae ex soluta nostrum
animi saepe eius itaque, voluptas, sapiente minima quo culpa explicabo
necessitatibus distinctio. Veritatis amet tempora, consectetur molestias
optio eveniet laudantium, tenetur aspernatur nobis ratione sit hic in
impedit quod deserunt recusandae atque, ipsam molestiae sequi!
</div>
</div>
</div>
</template>
We’re here displaying a card element with some text. We are also displaying the progress bar component (which we haven’t created yet) above it.
In the <script>
section, put this:
<script>
import ProgressBar from './ProgressBar'
export default {
components: { ProgressBar },
data () {
return {
progress: 0
}
}
}
</script>
We’ll use the progress
data property to store how much the user has scrolled so far. It will be from 0-1
.
Now we move to the <style>
section:
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 1px 0 0;
width: 100vw;
height: 100vh;
background: #6B5CA5;
}
html {
padding: 0;
margin: 0;
}
.card {
border-radius: 3px;
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.5;
font-size: 18px;
color: #444;
width: 300px;
margin: 10px auto;
height: 150px;
margin-top: 10px;
display: flex;
flex-direction: column;
background: #FFFFFF;
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
.text-section {
height: 100%;
max-height: 100%;
padding: 0 10px 10px;
overflow: scroll;
-webkit-overflow-scrolling: touch;
}
</style>
Step 2: Create ProgressBar.vue
Create ProgressBar.vue
and put this into <template>
:
<template>
<div class="progress-bar">
<div class="filled-bar"></div>
<span class="percentage-text"> Progress: {{ percentageText }} </span>
</div>
</template>
This component consists of three parts:
.progress-bar
is the container of the progress bar — we specify the width and the height of the progress bar here..filled-bar
will have the same width and height of its container, but it will be filled with a background color..percentage-text
displays the current scroll percentage.
.filled-bar
and .percentage-text
will be positioned absolutely. And, obviously, .percentage-text
should have a higher z-index
to appear above .filled-bar
.
As mentioned above, .filled-bar
will have the same width as its container. This means that we’ll use its position to reflect the current scroll percentage. So, when the user is at the very top (hasn’t scrolled yet), we’ll move the filled bar outside the progress bar container completely (from the left side). And as the user is scrolling down, we’ll move the filled bar to the right, to gradually enter the progress bar container.
But of course the user shouldn’t see the outside part of the filled bar. So we have to add overflow: hidden
to the container (.progress-bar
).
Now, let’s write the <script>
section:
<script>
export default {
props: {
value: {
type: Number,
default: 0
}
},
computed: {
percentageText () {
return `${Math.round(this.value * 100)}%`
}
}
}
</script>
Note how we’re accepting the current progress value through value
prop.
As you can see in the computed section, we’ve used that value
to get the percentage value as formatted text. So, for example, if the value
is 0.05
, it should output 5%
.
Next, let’s add the styling:
<style scoped>
.progress-bar {
position: relative;
height: 30px;
width: 100%;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border-radius: 3px 3px 0 0;
}
.percentage-text {
position: absolute;
top: 50%;
left: 50%;
z-index: 2;
font-size: 14px;
transform: translate3d(-50%, -50%, 0);
}
.filled-bar {
position: absolute;
top: 0;
left: 0;
z-index: 2;
height: 100%;
width: 100%;
background: #eef0ff;
}
</style>
Now if you view your work in the browser, you would see something like this:
We still have two things left to get this done:
- Update the filled bar position according to the current progress value (
value
prop). - Calculate the scroll percentage as the user’s scrolling. We’ll pass this value to the progress bar through the
value
prop.
Step 3: Update the filled bar position
We will use transform: translate3d(x, y, z)
CSS property to change the filled bar position.
Two things to notice here. First, we will only need to update the x
value and keep the other two 0. Second, we will use translate3d
instead of translateX
for performance reasons — to let the GPU handle it.
Now when x
is 0, the filled bar will appear in its original position (the user will see that the progress is completely filled). To move it completely to the left (outside the container), we should set it to -100%
. This means, x
will be from 0%-100%
.
We can use transform
on the filled bar simply by binding it to its style property:
<div
class="filled-bar"
:style="{ transform: `translate3d(-${(1 - value) * 100}%, 0, 0)` }"
></div>
You can test that the progress bar is working properly by changing the default value of the value
prop. Try to set it to 0.5
, and you should see it like this:
Step 4: Calculate the current scroll percentage
To get this value, we have to, first, listen to the @scroll
event on .text-section
, and then, divide the current scroll position (in pixels) by the full height of the text section.
So, in App.vue
, add @scroll
and ref
to .text-section
, like this:
<div class="text-section" ref="text" @scroll="onScroll"></div>
We needed to add ref
to access the element’s scroll position and height values.
Next, implement the onScroll
method:
methods: {
onScroll () {
const progress = this.$refs.text.scrollTop / (this.$refs.text.scrollHeight - this.$refs.text.clientHeight)
if (progress > 1) {
this.progress = 1
} else if (progress < 0) {
this.progress = 0
} else {
this.progress = progress
}
}
}
Because of the momentum scrolling on mobiles, we made sure that the progress value is not above 1
or below 0
— otherwise, the progress bar would look broken.
So, when the user scrolls, we update the progress
value in App.vue
; then, pass it to the progress bar component (through value
), which would then update the filled bar position and reproduce the formatted percentage text.