How to Build Scrolling Progress Lines with Vue and SVG
In this tutorial we will be building two SVG lines that keeps the vertical scroll position in a page whenever a user scrolls the page
We’ll use Vue 3, SVG, TailwindCSS, and the awesome @vueuse/core library. And don’t worry - if you’re just getting started with Vue or SVGs, we’ll explain everything as we go.
What do we want to build
The goal is pretty simple: build two vertical lines that has a white background and an orange foreground color that will track the scroll position in the page.
Here’s an example:
See the Pen Untitled by whatupnewyork (@whatupnewyork) on CodePen.
Step 1: Add SVG lines to your document
In your Vue component, start with the following template:
<template>
<div>
<svg class="fixed top-0 left-0 h-screen w-screen -z-10"
xmlns="http://www.w3.org/2000/svg">
<line x1="30" y1="0" x2="30" :y2="windowHeight" class="stroke-white" stroke-width="2" />
<line x1="30" y1="0" x2="30" :y2="redLinePosition" class="stroke-orange-500" stroke-width="2" />
</svg>
</div>
</template>
We’re using SVG to draw vertical lines. Let’s break down one of those lines:
<line x1="30" y1="0" x2="30" y2="800" />
y2="800"
means it ends at 800px down the screen - this is the part we’ll animate.y1="0"
means it starts at the top of the screen.x1="30"
andx2="30"
mean the line starts and ends atx = 30
pixels from the left. That keeps it vertical.
So we’re essentially saying: “Draw a white vertical line 2 pixels thick, starting at 30px from the left, stretching from the top down to the bottom.”
We add another <line>
element just on top of it with the same coordinates, but with a shorter y2
- this second line is orange and it grows as we scroll.
Step 2: Add Reactive Scroll Logic
Now let’s make the orange line grow as you scroll. Add the following script block:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useWindowSize } from '@vueuse/core'
const { width, height: windowHeight } = useWindowSize()
const redLinePosition = ref(0)
onMounted(() => {
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY
const scrollHeight = document.documentElement.scrollHeight
const clientHeight = window.innerHeight
const scrollProgress = scrollTop / (scrollHeight - clientHeight)
const lineY = scrollProgress * clientHeight
redLinePosition.value = lineY
})
})
</script>
redLinePosition
is the reactive variable controlling how long the orange line is. The longer you scroll, the bigger this value gets.useWindowSize()
from @vueuse/core, this gives you a reactive width value so the line on the right is always positioned correctly.scrollTop
represents how far you’ve scrolled.scrollHeight
is the total height of the whole page.clientHeight
is the visible height of the window (your screen).scrollProgress
is a number between 0 and 1 that tells us how much of the page you’ve scrolled.lineY
converts that progress into pixels so the orange line can grow accordingly.
Final results
You now have elegant scroll progress lines on both sides of your page!
As you scroll, the orange part grows. When you resize the window, the lines adjust automatically. All with clean Vue 3 reactivity and zero external animation libraries!
Here is the final component version:
<template>
<div>
<svg class="fixed top-0 left-0 h-screen w-screen -z-10" xmlns="http://www.w3.org/2000/svg">
<line x1="30" y1="0" x2="30" :y2="windowHeight" class="stroke-white" stroke-width="2" />
<line x1="30" y1="0" x2="30" :y2="redLinePosition" class="stroke-orange-500" stroke-width="2" />
</svg>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useWindowSize } from '@vueuse/core'
const { width, height: windowHeight } = useWindowSize()
const redLinePosition = ref(0)
onMounted(() => {
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY
const scrollHeight = document.documentElement.scrollHeight
const clientHeight = window.innerHeight
const scrollProgress = scrollTop / (scrollHeight - clientHeight)
const lineY = scrollProgress * clientHeight
redLinePosition.value = lineY
})
})
</script>