Transition and transitiongroup in Vue.js
Vue Transition - a built-in component that wraps a single element and applies CSS classes at the right moment so enter and leave animations work without any extra library.
Theory
TL;DR
<Transition>animates one element;<TransitionGroup>animates av-forlist- Vue injects six CSS classes automatically:
v-enter-from,v-enter-active,v-enter-toon enter, and the sameleavevariants on exit - Set
name="fade"and thev-prefix becomesfade- mode="out-in"prevents two components from overlapping during a swap- JavaScript hooks (
@enter,@leave) let you use GSAP or any animation library when CSS is not enough
Quick example
<template>
<button @click="show = !show">Toggle</button>
<Transition name="fade">
<p v-if="show">Hello!</p>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0; /* hidden at start of enter and end of leave */
}
</style>Vue adds .fade-enter-from and .fade-enter-active the frame the <p> enters the DOM. One frame later it adds .fade-enter-to and removes enter-from. When the transition ends, it cleans up all classes. The same flow runs in reverse on leave.
CSS class flow
Six classes follow a fixed sequence. The prefix is v- by default, or whatever you pass to name:
ENTER: v-enter-from → v-enter-active → v-enter-to
LEAVE: v-leave-from → v-leave-active → v-leave-to| Class | Applied when |
|---|---|
fade-enter-from | First frame of enter (start state) |
fade-enter-active | Entire enter duration |
fade-enter-to | Last frame of enter (end state) |
fade-leave-from | First frame of leave (start state) |
fade-leave-active | Entire leave duration |
fade-leave-to | Last frame of leave (end state) |
The -active class is where you write your transition or animation CSS. The -from and -to classes define the start and end states. That split lets you mix CSS transitions and CSS animations freely.
TransitionGroup for lists
<TransitionGroup> adds one thing that <Transition> cannot do: a move transition when list items rearrange. Vue uses the FLIP technique internally, recording positions before and after a change, then animating the difference via transform.
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>
<style>
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.3s ease; /* handles reorder animation */
}
.list-leave-active {
position: absolute; /* takes leaving item out of flow */
}
</style>The .list-move class is the key detail. Without it, items snap to new positions when one is removed. And position: absolute on .list-leave-active is needed because a leaving element still holds layout space while fading out. Taking it out of flow lets the remaining items slide into place.
Transition modes
By default <Transition> runs enter and leave at the same time. For component swaps that usually causes overlap. mode="out-in" fixes it.
<!-- current component leaves fully, then the next one enters -->
<Transition name="fade" mode="out-in">
<component :is="currentView" :key="currentView" />
</Transition>Always add :key on the component inside <Transition>. Without it, Vue may reuse the same DOM node and skip the animation entirely.
mode="in-out" does the opposite: new enters while old is still visible, then old leaves. Less common, but useful for layered overlay effects.
JavaScript hooks
CSS handles most cases. When you need a timeline, spring physics, or SVG morphing, use hooks and hand control to an animation library:
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@leave="onLeave"
:css="false"
>
<div v-if="show">Content</div>
</Transition>
<script setup>
import gsap from 'gsap'
function onBeforeEnter(el) {
gsap.set(el, { opacity: 0, y: -20 })
}
function onEnter(el, done) {
gsap.to(el, { opacity: 1, y: 0, duration: 0.4, onComplete: done })
}
function onLeave(el, done) {
gsap.to(el, { opacity: 0, y: 20, duration: 0.3, onComplete: done })
}
</script>Set :css="false" when using JS hooks exclusively. It tells Vue to skip class injection. If you skip this prop, Vue may call done too early and conflict with your library's timing.
Common mistakes
Missing :key on dynamic components
<!-- Vue reuses the DOM node - transition does not fire -->
<Transition name="fade" mode="out-in">
<component :is="currentView" />
</Transition>
<!-- correct -->
<Transition name="fade" mode="out-in">
<component :is="currentView" :key="currentView" />
</Transition>Forgetting position: absolute on .list-leave-active
Without it the leaving element holds space in the layout while fading. Other items do not move until Vue removes it, so reorder looks broken. This is the most common <TransitionGroup> issue I see in code reviews.
Not calling done() in JS hooks
The @enter and @leave hooks receive a done callback. If you never call it, Vue keeps the element in a transitioning state indefinitely. No further animations will fire on that element.
Wrapping multiple elements in <Transition>
<Transition> expects exactly one child. Two siblings inside it will cause a runtime warning. Wrap them in a single div or switch to <TransitionGroup>.
Real-world usage
- Modal dialogs:
<Transition name="modal" mode="out-in">wrapping the overlay - Tab views:
mode="out-in"on<component :is="activeTab"> - Notification toasts:
<TransitionGroup>with a slide-from-top name - Vue Router page transitions: wrap
<RouterView>in<Transition> - Staggered list animations:
@enterhook withel.dataset.indexto calculate per-item delay
Follow-up questions
Q: What is the difference between <Transition> and <TransitionGroup>?
A: <Transition> animates a single element on enter or leave. <TransitionGroup> animates a list and also handles move transitions when items reorder, using FLIP internally.
Q: Why does mode="out-in" exist?
A: Without it, enter and leave run simultaneously. For most view switches that causes two panels to overlap visually. out-in makes leave complete first, then enter starts.
Q: What happens if you forget to call done() in a JS hook?
A: Vue treats the transition as still running. The element stays stuck in its transitioning state and no subsequent transitions on it will trigger.
Q: How does <TransitionGroup> animate items moving to new positions?
A: It uses FLIP: records positions before and after the DOM change, applies an inverted transform so items appear to stay in place visually, then transitions to zero. The .list-move class controls that transition timing.
Q: When would you use :css="false"?
A: When JS hooks are doing all the animation work. It prevents Vue from adding transition classes that could interfere with your library's timing.
Examples
Basic fade toggle
<template>
<button @click="show = !show">Toggle message</button>
<Transition name="fade">
<p v-if="show" class="notice">Saved successfully.</p>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>Two CSS rules, no JavaScript. The notice fades in when show becomes true and fades out when it becomes false.
View switcher with out-in mode
<template>
<nav>
<button @click="view = 'HomeView'">Home</button>
<button @click="view = 'ProfileView'">Profile</button>
</nav>
<Transition name="slide" mode="out-in">
<component :is="view" :key="view" />
</Transition>
</template>
<style>
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-enter-from {
transform: translateX(20px);
opacity: 0;
}
.slide-leave-to {
transform: translateX(-20px);
opacity: 0;
}
</style>mode="out-in" makes the current view slide left and fade out before the next one slides in from the right. Remove mode and both animate at once, overlapping in the same space.
Sortable list with move animation
<template>
<button @click="shuffle">Shuffle cards</button>
<TransitionGroup name="cards" tag="ul" class="card-list">
<li v-for="card in cards" :key="card.id" class="card">
{{ card.label }}
</li>
</TransitionGroup>
</template>
<script setup>
import { ref } from 'vue'
const cards = ref([
{ id: 1, label: 'Card A' },
{ id: 2, label: 'Card B' },
{ id: 3, label: 'Card C' },
])
function shuffle() {
cards.value = [...cards.value].sort(() => Math.random() - 0.5)
}
</script>
<style>
.card-list {
position: relative; /* required for absolute-positioned leaving cards */
list-style: none;
padding: 0;
}
.cards-move {
transition: transform 0.4s ease;
}
.cards-enter-active,
.cards-leave-active {
transition: all 0.3s ease;
}
.cards-enter-from,
.cards-leave-to {
opacity: 0;
transform: scale(0.8);
}
.cards-leave-active {
position: absolute;
}
</style>Each shuffle triggers FLIP on every card that moved. The .cards-move class handles smooth repositioning. The parent position: relative is required so that absolute-positioned leaving cards stay anchored visually during their exit animation.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.