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

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 the beforeEach 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;
}