Vue.js JavaScript Animations
Introduction
While Vue's built-in transition system is powerful for many use cases, sometimes you need more control over your animations. That's where JavaScript animations come in. JavaScript animations in Vue.js allow you to create complex, dynamic, and interactive animations that may be difficult to achieve with CSS alone.
In this tutorial, you'll learn how to leverage Vue's JavaScript hooks to create custom animations, explore animation libraries integration, and build real-world animation examples that will make your applications more engaging and interactive.
Understanding Vue's JavaScript Animation Hooks
Vue's transition component provides JavaScript hooks that are triggered at different stages of a transition. These hooks give you full programmatic control over the animation process.
Available JavaScript Hooks
<Transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
<!-- content -->
</Transition>
Creating a Basic JavaScript Animation
Let's start with a simple example of a JavaScript animation using Vue's hooks. We'll animate an element's opacity and position.
<template>
<div>
<button @click="show = !show">Toggle</button>
<Transition
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<div v-if="show" class="js-animated-box"></div>
</Transition>
</div>
</template>
<script>
import { ref } from 'vue';
import gsap from 'gsap'; // We'll use GSAP library for this example
export default {
setup() {
const show = ref(true);
const beforeEnter = (el) => {
el.style.opacity = 0;
el.style.transform = 'translateX(-100px)';
};
const enter = (el, done) => {
gsap.to(el, {
opacity: 1,
translateX: 0,
duration: 1,
onComplete: done
});
};
const leave = (el, done) => {
gsap.to(el, {
opacity: 0,
translateX: 100,
duration: 0.8,
onComplete: done
});
};
return {
show,
beforeEnter,
enter,
leave
};
}
};
</script>
<style>
.js-animated-box {
width: 100px;
height: 100px;
background-color: #3eaf7c;
margin: 20px;
}
</style>
In this example:
beforeEnter
sets the initial state of the elemententer
animates the element to its final state when it appearsleave
animates the element before it's removed from the DOM
The important thing to note is that for enter
and leave
hooks, you need to call the done
callback when the animation completes. This signals to Vue that the transition is finished.
Combining CSS and JavaScript Animations
You can combine CSS transitions with JavaScript hooks for more complex animations:
<template>
<div>
<button @click="show = !show">Toggle</button>
<Transition
name="bounce"
@before-enter="beforeEnter"
@enter="enter"
>
<div v-if="show" class="combined-box"></div>
</Transition>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const show = ref(true);
const beforeEnter = (el) => {
el.style.transform = 'scale(0)';
};
const enter = (el, done) => {
// Create a small delay
setTimeout(() => {
// This triggers the CSS transition
el.style.transform = 'scale(1)';
// Wait for transition to finish before calling done
el.addEventListener('transitionend', done, { once: true });
}, 20);
};
return {
show,
beforeEnter,
enter
};
}
};
</script>
<style>
.combined-box {
width: 100px;
height: 100px;
background-color: #42b983;
margin: 20px;
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.bounce-enter-active {
transition: all 0.5s ease;
}
.bounce-leave-active {
transition: all 0.4s cubic-bezier(1, 0.5, 0.8, 1);
}
.bounce-enter-from,
.bounce-leave-to {
opacity: 0;
}
</style>
This example combines CSS transitions with JavaScript initialization to create a bouncy scale effect.
Integrating Animation Libraries
For more complex animations, integrating a dedicated animation library is often the best approach. Let's look at how to integrate three popular libraries:
Using GreenSock Animation Platform (GSAP)
GSAP is a professional-grade animation library that makes complex animations more manageable.
<template>
<div>
<button @click="show = !show">Toggle</button>
<button @click="playAnimation">Animate</button>
<Transition
@enter="onEnter"
@leave="onLeave"
>
<div v-if="show" class="gsap-box" ref="animatedBox"></div>
</Transition>
</div>
</template>
<script>
import { ref } from 'vue';
import gsap from 'gsap';
export default {
setup() {
const show = ref(true);
const animatedBox = ref(null);
const onEnter = (el, done) => {
gsap.from(el, {
y: -50,
opacity: 0,
scale: 0.5,
duration: 0.8,
ease: "bounce.out",
onComplete: done
});
};
const onLeave = (el, done) => {
gsap.to(el, {
y: 100,
opacity: 0,
duration: 0.6,
onComplete: done
});
};
const playAnimation = () => {
if (!animatedBox.value) return;
gsap.to(animatedBox.value, {
rotation: "+=360",
scale: 1.2,
duration: 1,
ease: "power1.inOut",
yoyo: true,
repeat: 1
});
};
return {
show,
animatedBox,
onEnter,
onLeave,
playAnimation
};
}
};
</script>
<style>
.gsap-box {
width: 100px;
height: 100px;
background-color: #ff7e67;
margin: 20px;
}
</style>
Using Anime.js
Anime.js is another lightweight animation library with a simple API.
<template>
<div>
<button @click="show = !show">Toggle</button>
<button @click="animatePath">Animate SVG</button>
<Transition
@enter="onEnter"
@leave="onLeave"
>
<div v-if="show" class="anime-box"></div>
</Transition>
<svg width="200" height="200">
<path
ref="path"
d="M10,80 C50,10 150,10 190,80 S130,150 10,80"
fill="none"
stroke="#42b983"
stroke-width="5"
/>
</svg>
</div>
</template>
<script>
import { ref } from 'vue';
import anime from 'animejs';
export default {
setup() {
const show = ref(true);
const path = ref(null);
const onEnter = (el, done) => {
anime({
targets: el,
translateY: [-50, 0],
opacity: [0, 1],
scale: [0.8, 1],
duration: 800,
easing: 'easeOutElastic(1, .5)',
complete: done
});
};
const onLeave = (el, done) => {
anime({
targets: el,
translateY: 50,
opacity: 0,
duration: 600,
easing: 'easeInQuad',
complete: done
});
};
const animatePath = () => {
anime({
targets: path.value,
d: [
'M10,80 C50,10 150,10 190,80 S130,150 10,80',
'M10,80 C80,30 120,30 190,80 S130,120 10,80'
],
duration: 1000,
easing: 'easeInOutQuad',
direction: 'alternate',
loop: true
});
};
return {
show,
path,
onEnter,
onLeave,
animatePath
};
}
};
</script>
<style>
.anime-box {
width: 100px;
height: 100px;
background-color: #42b983;
margin: 20px;
}
</style>
Real-World Example: Animated Page Transitions
Let's create a more practical example: animated page transitions in a Vue Router application.
<template>
<div class="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/contact">Contact</router-link>
</nav>
<Transition
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</Transition>
</div>
</template>
<script>
import { useRoute } from 'vue-router';
import { ref, watch } from 'vue';
import gsap from 'gsap';
export default {
setup() {
const route = useRoute();
const direction = ref('right');
// Determine animation direction based on route changes
watch(
() => route.path,
(newPath, oldPath) => {
const routeOrder = ['/', '/about', '/contact'];
const newIndex = routeOrder.indexOf(newPath);
const oldIndex = routeOrder.indexOf(oldPath);
direction.value = newIndex > oldIndex ? 'right' : 'left';
}
);
const beforeEnter = (el) => {
el.style.opacity = 0;
el.style.transform = `translateX(${direction.value === 'right' ? '100px' : '-100px'})`;
};
const enter = (el, done) => {
gsap.to(el, {
opacity: 1,
translateX: 0,
duration: 0.5,
ease: 'power2.out',
onComplete: done
});
};
const leave = (el, done) => {
gsap.to(el, {
opacity: 0,
translateX: direction.value === 'right' ? '-100px' : '100px',
duration: 0.5,
ease: 'power2.in',
onComplete: done
});
};
return {
beforeEnter,
enter,
leave
};
}
};
</script>
<style>
.app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
nav {
margin-bottom: 30px;
}
</style>
This example determines the animation direction based on the navigation flow between routes. If you move from Home to About, the content slides in from the right. If you go back, it slides in from the left.
Advanced Technique: Staggered Lists
A common UI pattern is to animate items in a list with a staggered delay. Here's how to accomplish this with Vue and GSAP:
<template>
<div>
<button @click="addItem">Add Item</button>
<button @click="removeItem">Remove Item</button>
<button @click="shuffleItems">Shuffle</button>
<TransitionGroup
tag="ul"
class="staggered-list"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<li v-for="(item, index) in items" :key="item.id" :data-index="index">
{{ item.text }}
</li>
</TransitionGroup>
</div>
</template>
<script>
import { ref } from 'vue';
import gsap from 'gsap';
export default {
setup() {
const items = ref([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' }
]);
let nextId = 4;
const beforeEnter = (el) => {
el.style.opacity = 0;
el.style.height = 0;
};
const enter = (el, done) => {
const delay = el.dataset.index * 0.15;
gsap.to(el, {
opacity: 1,
height: '3em',
delay,
duration: 0.4,
onComplete: done
});
};
const leave = (el, done) => {
const delay = el.dataset.index * 0.15;
gsap.to(el, {
opacity: 0,
height: 0,
delay,
duration: 0.4,
onComplete: done
});
};
const addItem = () => {
const pos = Math.floor(Math.random() * (items.value.length + 1));
items.value.splice(pos, 0, {
id: nextId++,
text: `Item ${nextId - 1}`
});
};
const removeItem = () => {
if (items.value.length > 0) {
const pos = Math.floor(Math.random() * items.value.length);
items.value.splice(pos, 1);
}
};
const shuffleItems = () => {
items.value = items.value
.slice()
.sort(() => Math.random() - 0.5);
};
return {
items,
beforeEnter,
enter,
leave,
addItem,
removeItem,
shuffleItems
};
}
};
</script>
<style>
.staggered-list {
list-style-type: none;
padding: 0;
}
.staggered-list li {
padding: 10px 20px;
margin: 5px 0;
background-color: #f5f5f5;
border-radius: 4px;
overflow: hidden;
}
</style>
This example creates a list where items animate in and out with a staggered delay based on their position in the list.
Performance Considerations
When working with JavaScript animations, keep these performance tips in mind:
-
Avoid animating expensive CSS properties: Properties like
width
,height
, andtop
can cause layout recalculations. Instead, prefer animatingtransform
andopacity
. -
Use
will-change
wisely: For complex animations, you can hint to the browser:
.animated-element {
will-change: transform, opacity;
}
-
Debounce window resize handlers: If your animations react to window size, debounce the resize event to prevent excessive calculations.
-
Clean up animations: Make sure to clean up any ongoing animations when components are unmounted:
onBeforeUnmount(() => {
// If using GSAP
gsap.killTweensOf(elementRef.value);
// Or if using timers
clearTimeout(timerRef.value);
});
Summary
JavaScript animations in Vue.js provide powerful capabilities for creating dynamic, engaging user interfaces. While CSS transitions are sufficient for simple effects, JavaScript animations give you more control and flexibility for complex interactions.
In this tutorial, you've learned:
- How to use Vue's transition JavaScript hooks
- How to integrate popular animation libraries like GSAP and Anime.js
- How to create real-world animations for page transitions and staggered lists
- Best practices for animation performance
With these techniques, you can create professional-grade animations that enhance the user experience of your Vue applications.
Additional Resources
To deepen your knowledge of Vue.js animations with JavaScript:
Exercises
- Create a card flip animation using JavaScript hooks that shows different content on each side.
- Implement a loading animation that uses SVG paths and JavaScript animation.
- Create a notification system where messages slide in, stay for a few seconds, and slide out.
- Build a photo gallery with zoom animations when clicking on images.
- Implement a staggered animation for a navigation menu that reveals submenu items with a cascade effect.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)