Skip to content

Instantly share code, notes, and snippets.

@markusand
Last active July 23, 2021 15:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markusand/bd502b019ba91f8f70b3a8f5cbb6f4d8 to your computer and use it in GitHub Desktop.
Save markusand/bd502b019ba91f8f70b3a8f5cbb6f4d8 to your computer and use it in GitHub Desktop.
Create fancy enter/leave animations in page elements, when switching between vue-router views.

Vue 3 Router Animations

Create fancy enter/leave animations in page elements, when switching between vue-router views.

Vue Router Animations

📦 Install

npm i gist:bd502b019ba91f8f70b3a8f5cbb6f4d8

Usage

Install VueRouterAnimations plugin, passing the router object as a parameter.

import router from './router';
import VueRouterAnimations from 'vue-router-animations';

app.use(VueRouterAnimations, { router });

Import default animations or create your own. Animations are a pair of -enter and -leave steps.

import 'vue-router-animations/animations/fade.css;
@keyframes my-custom-enter {
  from {
    opacity: 0;
    transform: rotate(180deg) translateX(100%) scale(0.5);
  }
  
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes my-custom-leave {
  from {
    opacity: 1;
    transform: none;
  }
  
  to {
    opacity: 0;
    transform: rotate(360deg) translate(-100%, 50%) scale(1.5); 
  }
}

Assign an animation to any element by defining custom data attributes or a vue directive instead. Other parameters such as duration and delay, both for enter and leave steps, can be defined using argument, modifiers and value or using a deep nested object as value.

<div v-routeranimation[:animation][.duration]="delay" />
<div v-routeranimation="options" />

Durations object must be defined at plugin config to allow using modifiers as duration shorthand.

app.use(VueRouterAnimations, {
  router,
  durations: { slow: '1s', fast: '0.25s' },
})
<div v-routeranimation:fade.slow />

Examples

<!-- Use my-custom animation -->
<img v-routeranimation:my-custom src="path.js">

<!-- Use fade animation with 2s delay -->
<img v-routeranimation:fade="'2s'" src="path.jpg">

<!-- Use different animations for enter/leave steps -->
<img v-routeranimation="{ enter: 'fade', leave: 'my-custom' }" src="path.jpg">

<!-- Advanced configuration -->
<img
   src="path.jpg"
   v-routeranimation="{
     enter: 'fade',
     duration: '1s',
     delay: `${Math.random() * 5}s`
     leave: {
       animation: 'my-custom',
       duration: '2s',
       delay: '0s',
     },
   }">
/* fade is imported by default */
@import 'reveal.css';
@import 'zoom.css';
:root {
--fade-offset: 25%;
}
@keyframes fade-enter {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fade-leave {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-down-enter {
from {
opacity: 0;
transform: translateY(calc(-1 * var(--fade-offset)));
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-down-leave {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(var(--fade-offset));
}
}
@keyframes fade-left-enter {
from {
opacity: 0;
transform: translateX(var(--fade-offset));
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fade-left-leave {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(calc(-1 * var(--fade-offset)));
}
}
@keyframes fade-right-enter {
from {
opacity: 0;
transform: translateX(calc(-1 * var(--fade-offset)));
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fade-right-leave {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(var(--fade-offset));
}
}
@keyframes fade-up-enter {
from {
opacity: 0;
transform: translateY(var(--fade-offset));
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-up-leave {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(calc(-1 * var(--fade-offset)));
}
}
import { nextTick } from 'vue';
import './fade.css'; // Default animation must always be included
const capitalize = str => `${str[0].toUpperCase()}${str.slice(1)}`;
/* DEFAULTS & CONSTANTS */
const ATTRIBUTES = ['animation', 'duration', 'delay'];
const DEFAULTS = {
namespace: 'routeranimation',
animation: 'fade',
duration: '1s',
delay: '0s',
};
/* VUE DIRECTIVE */
const Directive = config => {
const { namespace = DEFAULTS.namespace, durations = {} } = config;
return {
created: (el, { arg, modifiers, value = {} }) => {
// Find duration in directive modifiers (if exists)
const duration = durations[Object.keys(modifiers).find(m => m in durations)];
['enter', 'leave'].forEach(step => {
// Extract attributes
const attributes = {
animation: value[step]?.animation || value[step] || arg || DEFAULTS.animation,
duration: value[step]?.duration || value.duration || duration || DEFAULTS.duration,
delay: typeof value !== 'string'
? (value[step]?.delay || value.delay || DEFAULTS.delay)
: value,
};
// Set element data attributes
el.dataset[namespace] = true;
ATTRIBUTES.forEach(attr => {
const name = `${namespace}${capitalize(attr)}${capitalize(step)}`;
if (attributes[attr]) el.dataset[name] = attributes[attr];
});
});
},
};
};
/* ANIMATION SYSTEM */
const attachAnimations = (router, config = {}) => {
const CONFIG = { ...DEFAULTS, ...config };
const extract = (dataset, step) => ATTRIBUTES.reduce((acc, attr) => {
const key = `${CONFIG.namespace}${capitalize(attr)}`;
acc[attr] = dataset[`${key}${capitalize(step)}`] || dataset[key] || CONFIG[attr];
return acc;
}, {});
const getTargets = () => {
const selector = `[data-${CONFIG.namespace}]`;
const nodes = document.querySelectorAll(selector) || [];
return Array.from(nodes);
};
/* Trigger enter animation after new view is mounted */
router.afterEach(async () => {
await nextTick();
const targets = getTargets();
targets.forEach(target => {
const { animation, duration, delay } = extract(target.dataset, 'enter');
const property = `${animation}-enter ${duration} ease ${delay} backwards`;
target.style.setProperty('animation', property);
});
});
/* Trigger leave animation before unmount route view */
router.beforeEach(async () => {
const targets = getTargets();
const animations = targets.map(target => new Promise(resolve => {
const { animation, duration, delay } = extract(target.dataset, 'leave');
const property = `${animation}-leave ${duration} ease ${delay} forwards`;
target.style.setProperty('animation', property);
target.addEventListener('animationend', event => {
event.stopPropagation(); // Prevent trigger parent event
resolve();
});
}));
await Promise.all(animations);
});
};
/* EXPORT VUE PLUGIN */
export default {
install: (app, { router, ...config } = {}) => {
attachAnimations(router, config);
const { namespace = DEFAULTS.namespace } = config
app.directive(namespace, Directive(config));
},
};
{
"version": "1.0.1",
"name": "vue-router-animations",
"main": "index.js"
}
@keyframes reveal-down-enter {
from { clip-path: inset(0 0 100% 0); }
to { clip-path: inset(0); }
}
@keyframes reveal-down-leave {
from { clip-path: inset(0); }
to { clip-path: inset(100% 0 0 0); }
}
@keyframes reveal-left-enter {
from { clip-path: inset(0 0 0 100%); }
to { clip-path: inset(0); }
}
@keyframes reveal-left-leave {
from { clip-path: inset(0); }
to { clip-path: inset(0 100% 0 0); }
}
@keyframes reveal-right-enter {
from { clip-path: inset(0 100% 0 0); }
to { clip-path: inset(0); }
}
@keyframes reveal-right-leave {
from { clip-path: inset(0); }
to { clip-path: inset(0 0 0 100%); }
}
@keyframes reveal-up-enter {
from { clip-path: inset(100% 0 0 0); }
to { clip-path: inset(0); }
}
@keyframes reveal-up-leave {
from { clip-path: inset(0); }
to { clip-path: inset(0 0 100% 0); }
}
:root {
--zoom-offset: 0.5;
}
@keyframes zoom-in-enter {
from {
opacity: 0;
transform: scale(calc(1 - var(--zoom-offset)));
}
to {
opacity: 1;
}
}
@keyframes zoom-in-leave {
from {
opacity: 1;
transform: none;
}
to {
opacity: 0;
transform: scale(calc(1 + var(--zoom-offset)));
}
}
@keyframes zoom-out-enter {
from {
opacity: 0;
transform: scale(calc(1 + var(--zoom-offset)));
}
to {
opacity: 1;
}
}
@keyframes zoom-out-leave {
from {
opacity: 1;
transform: none;
}
to {
opacity: 0;
transform: scale(calc(1 - var(--zoom-offset)));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment