Skip to content

VueJS - Transition & Animation

In this chapter, we will explore how to apply engaging transitions and animations to elements in your VueJS applications when they are added, updated, or removed from the DOM.

Vue provides a built-in <transition> wrapper component. You can wrap this component around any element or component that enters or leaves the DOM (typically using directives like v-if, v-show, or for dynamic components).

<transition name="my-transition">
<p v-if="show">Hello</p>
</transition>

When an element wrapped in <transition> is inserted or removed, Vue automatically adds/removes CSS classes at different stages of the transition. The name attribute (e.g., my-transition) serves as a prefix for these classes. If no name is provided, the prefix defaults to v-.

Six classes are applied for enter/leave transitions:

  • Enter Phase:
  • v-enter-from: Starting state for enter. Added before the element is inserted, removed one frame after insertion.
  • v-enter-active: Active state for enter. Applied during the entire entering phase. Used to define duration, delay, and easing curve. Removed when the transition finishes.
  • v-enter-to: Ending state for enter. Added one frame after insertion (at the same time v-enter-from is removed), removed when the transition finishes.
  • Leave Phase:
  • v-leave-from: Starting state for leave. Added immediately when a leaving transition is triggered, removed after one frame.
  • v-leave-active: Active state for leave. Applied during the entire leaving phase. Used to define duration, delay, and easing curve. Removed when the transition finishes.
  • v-leave-to: Ending state for leave. Added one frame after the leaving transition is triggered (at the same time v-leave-from is removed), removed when the transition finishes.

If you used a name attribute like name="fade", the classes would be .fade-enter-from, .fade-enter-active, etc.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 Transition Example</title>
<script src="https://unpkg.com/vue@3"></script>
<style>
/* Base styles for the element */
.demo-p {
font-size: 20px;
color: darkcyan;
}
/* Define transition states */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* Optional: Define enter-to and leave-from if needed,
often opacity: 1 is the default */
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
</style>
</head>
<body>
<div id="app">
<button @click="show = !show">Toggle Text</button>
<transition name="fade">
<p class="demo-p" v-if="show">Animation Example</p>
</transition>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const show = ref(true);
return { show };
}
}).mount('#app');
</script>
</body>
</html>

In this example:

  • We use Vue 3’s Composition API (createApp, ref).
  • The <p> tag is conditionally rendered using v-if="show".
  • It’s wrapped in <transition name="fade">.
  • The CSS classes .fade-enter-active and .fade-leave-active define the transition duration and easing.
  • .fade-enter-from and .fade-leave-to define the state where the element is invisible (opacity 0).

When the ‘Toggle Text’ button is clicked, the show ref changes. If show becomes true, the paragraph fades in over 0.5 seconds. If show becomes false, it fades out.

You can also use CSS animations instead of transitions. The usage with the <transition> component is similar, but Vue waits for the animationend event instead of transitionend. You typically define the animation in the *-enter-active or *-leave-active classes.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 Animation Example</title>
<script src="https://unpkg.com/vue@3"></script>
<style>
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse; /* Use reverse animation for leave */
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}
.demo-div {
width: 100px;
height: 100px;
background-color: tomato;
border-radius: 50%;
margin-top: 20px;
}
</style>
</head>
<body>
<div id="app">
<button @click="show = !show">Toggle Ball</button>
<transition name="bounce">
<div class="demo-div" v-if="show"></div>
</transition>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const show = ref(true);
return { show };
}
}).mount('#app');
</script>
</body>
</html>

Here, we define a @keyframes animation named bounce-in. We apply this animation to the .bounce-enter-active class. For the leaving animation (.bounce-leave-active), we apply the same animation but in reverse direction. When the button is toggled, the red circle appears with a bouncing effect or disappears with a reverse bounce.

Using Animation Libraries (e.g., Animate.css)

Section titled “Using Animation Libraries (e.g., Animate.css)”

You can easily integrate third-party CSS animation libraries like Animate.css. Instead of defining the transition classes yourself, you can provide custom class names directly to the <transition> component using specific attributes.

  • enter-from-class
  • enter-active-class
  • enter-to-class
  • leave-from-class
  • leave-active-class
  • leave-to-class

These attributes have higher priority than the regular class names and are especially useful for combining Vue’s transitions with libraries like Animate.css.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 Animate.css Example</title>
<!-- Use a modern version of Animate.css -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<script src="https://unpkg.com/vue@3"></script>
<style>
.demo-p {
font-size: 24px;
font-weight: bold;
color: steelblue;
}
</style>
</head>
<body>
<div id="app">
<button @click="show = !show">Toggle with Animate.css</button>
<transition
enter-active-class="animate__animated animate__fadeInDown"
leave-active-class="animate__animated animate__fadeOutUp"
>
<p class="demo-p" v-if="show">Using Animate.css!</p>
</transition>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const show = ref(true);
return { show };
}
}).mount('#app');
</script>
</body>
</html>

In this code, we include the Animate.css library. We then use the enter-active-class and leave-active-class attributes on the <transition> component. We provide the necessary Animate.css classes (animate__animated is required, plus the specific animation like animate__fadeInDown). Now, toggling the paragraph uses the predefined animations from the library.

Vue automatically detects when the transition or animation has ended by listening for transitionend or animationend events. However, if you have complex transitions (e.g., a CSS transition mixed with a CSS animation whose duration differs), or if Vue fails to detect the duration correctly, you can specify an explicit duration using the duration prop.

<!-- Same duration for enter and leave -->
<transition :duration="1000">...</transition>
<!-- Different durations for enter and leave -->
<transition :duration="{ enter: 500, leave: 800 }">...</transition>

The duration is specified in milliseconds.

For more complex animations or scenarios where CSS transitions/animations are not sufficient (e.g., animating element properties that CSS can’t handle directly, coordinating multiple elements), you can use JavaScript hooks. You define methods and bind them to <transition> component events.

  • @before-enter(el)
  • @enter(el, done): Important: Call done() to signal the end of the enter animation.
  • @after-enter(el)
  • @enter-cancelled(el)
  • @before-leave(el)
  • @leave(el, done): Important: Call done() to signal the end of the leave animation.
  • @after-leave(el)
  • @leave-cancelled(el) (only available with v-show)

When using only JavaScript hooks (no CSS transitions/animations), it’s good practice to add :css="false" to the <transition> component. This tells Vue to skip the CSS detection, potentially improving performance slightly and preventing CSS rules from interfering.

Example: JS Hook with Simple Opacity Change

Section titled “Example: JS Hook with Simple Opacity Change”
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 JS Hooks Example</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<button @click="show = !show">Toggle with JS</button>
<transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@leave="onLeave"
:css="false" /* Tell Vue we are handling animations via JS */
>
<p style="font-size: 20px; color: green;" v-if="show">
Animating with JavaScript!
</p>
</transition>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const show = ref(false);
const onBeforeEnter = (el) => {
// Set initial state before enter
el.style.opacity = 0;
el.style.transform = 'translateY(20px)';
};
const onEnter = (el, done) => {
// Start the animation
// Using simple requestAnimationFrame for demo
let start;
const duration = 500; // ms
function step(timestamp) {
if (start === undefined) start = timestamp;
const elapsed = timestamp - start;
const progress = Math.min(elapsed / duration, 1);
el.style.opacity = progress;
el.style.transform = `translateY(${20 * (1 - progress)}px)`;
if (progress < 1) {
requestAnimationFrame(step);
} else {
done(); // Signal animation completion
}
}
requestAnimationFrame(step);
};
const onLeave = (el, done) => {
// Start the leave animation (similar logic, reversing opacity/transform)
let start;
const duration = 300; // ms
function step(timestamp) {
if (start === undefined) start = timestamp;
const elapsed = timestamp - start;
const progress = Math.min(elapsed / duration, 1);
el.style.opacity = 1 - progress;
el.style.transform = `translateY(${20 * progress}px)`;
if (progress < 1) {
requestAnimationFrame(step);
} else {
done(); // Signal animation completion
}
}
requestAnimationFrame(step);
};
return { show, onBeforeEnter, onEnter, onLeave };
}
}).mount('#app');
</script>
</body>
</html>

This example uses requestAnimationFrame for a basic JavaScript animation controlling opacity and position. Libraries like GSAP (GreenSock Animation Platform) are often used for more complex JS animations, providing powerful tweening capabilities and timeline control. Remember to call the done callback in @enter and @leave when the animation is complete.

Common Pitfall: Forgetting to call the done callback in @enter and @leave hooks will cause the transition to hang indefinitely.

To apply a transition when the component first mounts (initial render), add the appear attribute to the <transition> component.

<transition name="fade" appear>
<p v-if="true">Fades in on load</p>
</transition>

You can also specify custom CSS classes or JavaScript hooks specifically for the appear transition using attributes like appear-active-class, appear-from-class, appear-to-class, or JS hooks like @before-appear, @appear, @after-appear, @appear-cancelled.

The <transition> component can also handle transitions when switching between elements using v-if/v-else-if/v-else, or when switching dynamic components via <component :is="...">. However, a common requirement is that both elements must have a unique key attribute for Vue to correctly track and transition them.

<transition name="fade" mode="out-in">
<button v-if="isEditing" key="edit">Edit</button>
<button v-else key="save">Save</button>
</transition>

The mode attribute controls the timing: out-in (current element transitions out first, then new element transitions in) or in-out (new element transitions in first, then current element transitions out). out-in is generally recommended for smoother visual results.

For animating lists rendered with v-for, Vue provides the <TransitionGroup> component. It renders a real DOM element (defaulting to <span>, configurable with the tag prop) and applies transitions to the items inside.

Key differences from <transition>:

  • It renders an actual element (e.g., <ul>, <div>) instead of being abstract.
  • Transition classes are applied to the inner elements, not the group element itself.
  • Each item in the list MUST have a unique key attribute.
  • It supports move transitions (v-move class) for when the list order changes, which requires using CSS transform.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 TransitionGroup Example</title>
<script src="https://unpkg.com/vue@3"></script>
<style>
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}
/* Ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.list-leave-active {
position: absolute;
}
</style>
</head>
<body>
<div id="app">
<button @click="addItem">Add Item</button>
<button @click="removeItem">Remove Item</button>
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id" class="list-item">
{{ item.text }}
</li>
</transition-group>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const items = ref([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' }
]);
let nextId = 4;
const addItem = () => {
const randomIndex = Math.floor(Math.random() * items.value.length);
items.value.splice(randomIndex, 0, { id: nextId++, text: `Item ${nextId -1}` });
};
const removeItem = () => {
if (items.value.length > 0) {
const randomIndex = Math.floor(Math.random() * items.value.length);
items.value.splice(randomIndex, 1);
}
};
return { items, addItem, removeItem };
}
}).mount('#app');
</script>
</body>
</html>

This example shows adding and removing items from a list with a fade and slide transition. Note the use of key on each li and the tag="ul" prop on <transition-group>. The .list-leave-active { position: absolute; } style is important for smooth move animations when items are removed.

Further Reading: Vue’s official documentation provides in-depth details on transitions: https://vuejs.org/guide/built-ins/transition.html and list transitions: https://vuejs.org/guide/built-ins/transition-group.html