Transitions in Vue are great. As a built-in feature, they allow developers to quickly incorporate smooth navigation animations. In this article, we'll take it even further and build a dedicated loading transition using an intermediate component
Table of contents
TL:DR - check out this codesandbox for the article's source code
Getting started
Let's fire up your favorite editor's terminal and create a new Vue project using vue@latest
:
npm create vue@latest .
Select Router
as an additional dependency during setup and run npm run install
after the project has been scaffolded. Finally, get rid of all views and components created by the script, and you're ready to start.
Boilerplate
Let's add some simple views we can navigate between next. Inside the views
folder, create these three components:
- HomeView.vue
- AboutView.vue
- ShopView.vue
Add the following code to each and adjust the title, as well as the paragraph:
<script setup>
</script>
<template>
<main>
<h1>This is the home / about / shop page</h1>
<p>This is the home / about / shop page</p>
</main>
</template>
The App.vue file is next. Let's add some basic structure, styles, and a placeholder for our transition logic.
<script setup></script>
<template>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/shop">Shop</RouterLink>
</nav>
<transition name="fade">
<Loading v-if="loading"></Loading>
</transition>
<main>
<RouterView />
</main>
</template>
<style></style>
Styles:
body {
background-color: #222;
color: #eee;
padding: 1rem;
overflow-x: hidden;
}
nav {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
nav a {
color: #eee;
text-decoration: none;
font-weight: bold;
font-size: 1.2rem;
padding: 0.5rem 1rem;
border-radius: 4px;
}
nav a:hover,
nav a.router-link-active {
background-color: steelblue;
}
main {
margin-top: 2rem;
text-align: center;
}
Finally, let's adjust the router/index.js
file to incorporate all relevant views.
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
},
{
path: '/shop',
name: 'shop',
component: () => import('../views/ShopView.vue'),
},
],
})
export default router
Now that all preparations are in place, let's start implementing the transition component
Adding the loading component
The component we're about to build will slide in from the right whenever we change the route and slide out after a specified time. To keep it simple, we'll animate the Vue logo from the boilerplate. It will look like this:

Let's start with the template code.
- Create a new file in the components directory and name it
Loading.vue
. - Add an image with two filters.
- The filter's values depend on the current loading percentage
- Below, is a simple text to indicate the component's purpose
<script setup></script>
<template>
<div class="loading-container">
<img :style="`filter: contrast(${loadingPercentage / 100}) blur(${12.5 - loadingPercentage/8}px)`" height="240px" width="240px" src="@/assets/logo.svg" alt="logo">
<h2>More content is on the way, stand by</h2>
</div>
</template>
<style scoped></style>
- We'll have to add the variable of loading percentage next
- Also, two handlers are needed to set the value dynamically once the component is loaded and unloaded
- Let's add them to the script section
import { ref, onMounted, onUnmounted } from 'vue';
const loadingPercentage = ref(0);
onMounted(() => {
const interval = setInterval(() => {
loadingPercentage.value += 2.5;
if (loadingPercentage.value >= 100) {
clearInterval(interval);
}
}, 40);
})
onUnmounted(() => {
loadingPercentage.value = 0
})
- Let's wrap up this section by adding some styles
- Especially the
position: absolute
is important to keep the UI clearly arranged - Add the
loading-container
class to the styles section
.loading-container {
background-color: #0a0a0a;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 0.5rem;
position: absolute;
top: 0;
left: 0;
}
Finalize the App.vue file
All that's left to do is add the loading logic and the transition itself to the App.vue file.
- We need to import the
useRouter
composable and add a function to thebeforeEach
router lifecycle hook - Doing this, however, runs the transition when the component is initially rendered, which happens whenever a user opens the app in their browser
- To prevent this from happening, we'll add an indicator variable that prevents the initial rendering altogether
Note that you do not have to set the loading values statically, you can easily bind other values or even network composables into the lifecycle hook, as well as into the component itself using properties
Add the following to the App.vue
's script section:
import { ref } from 'vue';
import { RouterLink, RouterView, useRouter } from 'vue-router';
import Loading from './components/Loading.vue'
const loading = ref(false);
const initiallyLoaded = ref(false);
useRouter().beforeEach(() => {
if (initiallyLoaded.value === false) {
initiallyLoaded.value = true;
return;
}
loading.value = true
setTimeout(() => {
loading.value = false
}, 2500)
})
All that's left to do now is to add the transition CSS code to create a smooth sliding transition.
.fade-enter-active,
.fade-leave-active {
transition: all 0.75s ease;
}
.fade-enter-from {
transform: translateX(100vw);
opacity: 0;
}
.fade-leave-to {
transform: translateX(-100vw);
opacity: 0;
}