.

Using a custom loader in Nuxt to create a page transition

It's possible to use a custom loader as a transition component in Nuxt and it's actually really easy to setup and use. And it feels like fairly clean way to do something fun with your transitions.

So below is the code to create the transition used on this site.

First let's tell nuxt to use a custom loader, add the following line in your nuxt.config.js:

loading: '~/components/Loader.vue',

Now lets have a look in the Loader.vue

First up the template part:

<div v-if="loading" :class="[ 'page-transition', { 'animate-in': animateIn }, { 'animate-out': animateOut }, ]" > <div :class="[ 'page-transition__inner', { 'animate-in': animateIn }, { 'animate-out': animateOut }, ]" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" > <circle v-for="(item, index) in points" :key="index" :cx="item.x" :cy="item.y" :r="item.width" :fill="item.color" /> </svg> </div> </div>

Very simple html here. Setting some dynamtic classes and drawing some circles based on an array of objects.

The Script

The nuxt loader has a couple of native methods, start() and finish() which I use to handle the state and generate 20 random circles on the in the transition so it changes every time. So the script part of my code looks like this.

import { random } from '@georgedoescode/generative-utils'; export default { data() { return { loading: false, animateIn: false, animateOut: false, points: [], }; }, methods: { start() { this.loading = true; this.animateIn = true; this.drawStuff(); }, finish() { setTimeout(() => { this.animateIn = false; this.animateOut = true; }, 800); setTimeout(() => { this.loading = false; this.animateOut = false; }, 1600); }, drawStuff() { let points; const width = 200; const height = 200; const color = ['#FF7F6A', '#FE4365', '#B4C8C2', '#ffffff']; points = [...Array(20)].map(() => { return { x: random(0, width), y: random(0, height), width: random(2, 20), height: random(2, 20), color: random(color), }; }); this.points = points; }, }, };

I'm using the random function from George Francis' generative utils to handle the some of the generating of the circles. Other than that I'm setting some states which activates the transitions and I'm using a couple of timeouts to make sure the transitions are not cut short.

Lastly the CSS

/* Default page transition to make sure content is loaded in after the overlay is covering */ .page-enter-active, .page-leave-active { transition: transform 500ms ease; } .page-enter-active { transition-delay: 300ms; } .page-enter, .page-leave-to { transform: translateY(0px); } .page-enter, .page-leave-to { transition-delay: none; } .page-transition { position: fixed; @apply bg-color-5; bottom: 0; top: auto; left: 0; width: 100vw; height: 0; display: flex; align-items: center; justify-content: center; z-index: 2000; overflow: hidden; } .page-transition__inner { color: white; font-size: 10vw; opacity: 0; } .page-transition__inner svg { width: 100vw; height: 100vh; display: block; } .page-transition.animate-in { animation: 800ms cubic-bezier(0.535, 0, 0, 1) slideIn; animation-fill-mode: forwards; } .page-transition.animate-out { bottom: auto; top: 0; animation: 800ms cubic-bezier(0.535, 0, 0, 1) slideOut; } .page-transition__inner.animate-in { animation: 800ms cubic-bezier(0.535, 0, 0, 1) fadeInUp; animation-fill-mode: forwards; } .page-transition__inner.animate-out { animation: 800ms cubic-bezier(0.535, 0, 0, 1) fadeOutUp; } @keyframes slideIn { from { height: 0; } to { height: 100%; } } @keyframes slideOut { from { height: 100%; } to { height: 0; } } @keyframes fadeInUp { from { opacity: 0; transform: translateY(-200px); } to { opacity: 0.7; transform: translateY(0); } } @keyframes fadeOutUp { from { opacity: 0.7; transform: translateY(0); } to { opacity: 0; transform: translateY(200px); } }

There's som default page transition styles at the top to prevent the page content from changing before animateIn is done. And the rest to just to handle the animation in and out.

Loader.vue - All together

<template lang="html"> <div v-if="loading" :class="[ 'page-transition', { 'animate-in': animateIn }, { 'animate-out': animateOut }, ]" > <div :class="[ 'page-transition__inner', { 'animate-in': animateIn }, { 'animate-out': animateOut }, ]" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" > <circle v-for="(item, index) in points" :key="index" :cx="item.x" :cy="item.y" :r="item.width" :fill="item.color" /> </svg> </div> </div> </template> <script> import { random } from '@georgedoescode/generative-utils'; export default { data() { return { loading: false, animateIn: false, animateOut: false, points: [], }; }, methods: { start() { this.loading = true; this.animateIn = true; this.drawStuff(); }, finish() { setTimeout(() => { this.animateIn = false; this.animateOut = true; }, 800); setTimeout(() => { this.loading = false; this.animateOut = false; }, 1600); }, drawStuff() { let points; const width = 200; const height = 200; const color = ['#FF7F6A', '#FE4365', '#B4C8C2', '#ffffff']; points = [...Array(20)].map(() => { return { x: random(0, width), y: random(0, height), width: random(2, 20), height: random(2, 20), color: random(color), }; }); this.points = points; }, }, }; </script> <style lang="scss"> /* Default page transition to make sure content is loaded in after the overlay is covering */ .page-enter-active, .page-leave-active { transition: transform 500ms ease; } .page-enter-active { transition-delay: 300ms; } .page-enter, .page-leave-to { transform: translateY(0px); } .page-enter, .page-leave-to { transition-delay: none; } .page-transition { position: fixed; @apply bg-color-5; bottom: 0; top: auto; left: 0; width: 100vw; height: 0; display: flex; align-items: center; justify-content: center; z-index: 2000; overflow: hidden; } .page-transition__inner { color: white; font-size: 10vw; opacity: 0; } .page-transition__inner svg { width: 100vw; height: 100vh; display: block; } .page-transition.animate-in { animation: 800ms cubic-bezier(0.535, 0, 0, 1) slideIn; animation-fill-mode: forwards; } .page-transition.animate-out { bottom: auto; top: 0; animation: 800ms cubic-bezier(0.535, 0, 0, 1) slideOut; } .page-transition__inner.animate-in { animation: 800ms cubic-bezier(0.535, 0, 0, 1) fadeInUp; animation-fill-mode: forwards; } .page-transition__inner.animate-out { animation: 800ms cubic-bezier(0.535, 0, 0, 1) fadeOutUp; } @keyframes slideIn { from { height: 0; } to { height: 100%; } } @keyframes slideOut { from { height: 100%; } to { height: 0; } } @keyframes fadeInUp { from { opacity: 0; transform: translateY(-200px); } to { opacity: 0.7; transform: translateY(0); } } @keyframes fadeOutUp { from { opacity: 0.7; transform: translateY(0); } to { opacity: 0; transform: translateY(200px); } } </style>

I hope you found this useful. There are so many others ways to handle your transitions, but I wanted to share this as I didn't know this was possible before very recently.