Transitions & Animations
<Transition>, <TransitionGroup>, enter/leave classes and list animations.
Animation is how an interface explains itself: a panel slides in from where you clicked, a deleted row fades out instead of vanishing, and the rest of the list glides up to fill the gap. Vue has two built-in components for this: <Transition> for one element entering or leaving, and <TransitionGroup> for lists.
Neither component animates anything itself. They add and remove CSS classes at the right moments, and your CSS does the animating. That keeps animations fast (the browser runs them on the compositor) and lets you use any CSS you like.
How <Transition> works
Wrap an element that appears or disappears through v-if, v-show, a dynamic component or a changing key:
<Transition name="fade">
<p v-if="show">Hello</p>
</Transition>
When show changes, Vue applies six classes, prefixed with the name:
So a fade needs just two rules:
.fade-enter-active,
.fade-leave-active { transition: opacity 0.4s ease; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
The "to" states don't need rules here because the element's normal style (opacity: 1) is already the end state.
Fade, slide and bounce
Three of the most common transitions. The name selects the CSS:
/* slide: drops in from above, leaves downwards */
.slide-enter-active, .slide-leave-active { transition: all 0.35s ease; }
.slide-enter-from { opacity: 0; transform: translateY(-14px); }
.slide-leave-to { opacity: 0; transform: translateY(14px); }
/* bounce: uses a keyframe animation instead of a transition */
.bounce-enter-active { animation: bounce-in 0.5s; }
.bounce-leave-active { animation: bounce-in 0.35s reverse; }
@keyframes bounce-in {
0% { transform: scale(0); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}
Animate opacity and transform wherever you can. The browser can animate both without recalculating layout, so they stay smooth even on slow phones. Animating height, width, top or margin forces a layout recalculation on every frame.
A dismissible alert
The fade and slide classes above are loaded on this page, so you can use them here. Change name="fade" to name="slide" or name="bounce" and press Run:
Switching between elements: mode
When one element replaces another (an "Edit" button becoming "Saving…"), both exist during the transition. By default they animate at the same time, overlapping and pushing each other around. mode="out-in" waits for the old one to leave before the new one enters:
<Transition name="slide" mode="out-in">
<button :key="label" @click="next">{{ label }}</button>
</Transition>
The :key is essential. It's what tells Vue this is a different element. Without it, Vue simply patches the text in place and nothing animates.
out-in is what you want almost every time. in-out (new enters first, then the old leaves) is rarely useful.
Animating lists: <TransitionGroup>
<TransitionGroup> animates items added to or removed from a v-for list. It also adds a *-move class to items that change position, so the rest of the list glides into place instead of jumping:
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
</TransitionGroup>
.list-move, /* items sliding to their new position */
.list-enter-active,
.list-leave-active { transition: all 0.45s ease; }
.list-enter-from,
.list-leave-to { opacity: 0; transform: translateX(30px); }
/* take leaving items out of the layout so the others can move up smoothly */
.list-leave-active { position: absolute; left: 0; right: 0; }
Three rules for lists:
- Every item needs a stable, unique
key, like a database id. With the array index as the key, Vue can't tell which item moved, so the move animation breaks. tag="ul"sets the wrapper element. Without it,<TransitionGroup>renders no wrapper.- The
position: absoluteon leaving items is what makes the gap close smoothly. The list's container needsposition: relative.
JavaScript hooks: animating to height: auto
CSS can't transition to height: auto, so "expand this panel to fit its content" needs JavaScript. <Transition> emits hooks at each stage, and :css="false" tells Vue not to wait for CSS:
<Transition :css="false" @before-enter="onBeforeEnter" @enter="onEnter" @leave="onLeave">
<div v-if="open">…any amount of content…</div>
</Transition>
function onBeforeEnter(el) {
el.style.height = '0px';
el.style.overflow = 'hidden';
}
function onEnter(el, done) {
// measure the real height, then animate to it with the Web Animations API
const anim = el.animate(
[{ height: '0px' }, { height: `${el.scrollHeight}px` }],
{ duration: 300, easing: 'ease-out' },
);
anim.onfinish = () => { el.style.height = ''; done(); }; // call done() when finished
}
function onLeave(el, done) {
el.animate(
[{ height: `${el.scrollHeight}px` }, { height: '0px' }],
{ duration: 250, easing: 'ease-in' },
).onfinish = done;
}
The done callback tells Vue the animation has finished. On leave, the element is removed only once you call it. The same hooks are where you'd plug in an animation library such as GSAP.
Reusable transitions
Once you settle on a house style, wrap it in a component so nobody has to remember class names:
<!-- components/FadeTransition.vue -->
<template>
<Transition name="fade" mode="out-in" appear>
<slot />
</Transition>
</template>
<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
appear also animates the element on first render, which is useful for page content. The same pattern works with Vue Router to animate between pages:
<RouterView v-slot="{ Component }">
<FadeTransition>
<component :is="Component" />
</FadeTransition>
</RouterView>
Respect users who've asked their system for less motion. One CSS rule turns your transitions off for them:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}
Key takeaways
<Transition>and<TransitionGroup>add classes at the right moments. Your CSS does the animating.- The six classes are
{name}-enter-from / -enter-active / -enter-toand the same forleave. Put thetransitionproperty on*-active. - When one element replaces another, give it a
:keyand usemode="out-in". - Lists need stable keys.
*-moveanimates reordering, andposition: absoluteon leaving items closes gaps smoothly. - Use JavaScript hooks with
:css="false"for things CSS can't do, like animating toheight: auto. - Animate
opacityandtransformfor smoothness, and honourprefers-reduced-motion.