Chapter 11 of 11

Vue 3 tutorial: animate with Transition and TransitionGroup

verified on 2 September 2026 · 5 min

Quick answer

<Transition> animates a single element as it appears and disappears, <TransitionGroup> animates a list. Vue applies CSS classes, your stylesheet decides the effect. Careful: in Vue 3 the starting classes are -enter-from and -leave-from, not -enter and -leave as in Vue 2.

By the end of this chapter, you will know how to animate elements appearing, disappearing and moving, without any library.

An element that appears all at once produces a jarring jump. Vue ships two built-in components, <Transition> and <TransitionGroup>, which apply CSS classes at the right moment to animate entries and exits. No library to install.

The principle

Wrap an element whose display depends on a condition in <Transition>. Vue does not animate it itself: it adds and removes classes, and your CSS decides the effect.

Six classes cover the whole cycle.

css
/* enter */
.v-enter-from     /* starting state, applied just before insertion */
.v-enter-active   /* during the whole entry: this is where transition goes */
.v-enter-to       /* end state */

/* leave */
.v-leave-from     /* starting state of the exit */
.v-leave-active   /* during the whole exit */
.v-leave-to       /* final state, just before removal from the DOM */

The v- prefix is the one of an unnamed transition. With name="fade", the classes become fade-enter-from, fade-enter-active, and so on.

The trap in code found on the web

In Vue 2, the starting classes were called .fade-enter and .fade-leave, with no suffix. Vue 3 renamed them -enter-from and -leave-from. An example copied from an old article will therefore produce no entry animation at all, without the slightest error message. If your transition does nothing, check this point first.

Animating availability

In ProductDisplay.vue, the stock message switches between two paragraphs depending on the hovered variant. Wrap them:

src/components/ProductDisplay.vue
<Transition name="fade" mode="out-in">
  <p v-if="inStock" key="in">En stock</p>
  <p v-else key="out">En rupture</p>
</Transition>

Then describe the effect in the stylesheet, as in the chapter on classes and styles:

src/assets/boutique.css
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
  transform: translateY(-10px);
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity .3s ease, transform .3s ease;
}

Hover over the colour dots: the message fades instead of jumping.

mode="out-in" waits for the old element to leave before inserting the new one. Without it, the two paragraphs coexist for a split second and overlap.

What Transition accepts

One element at a time. That is the main constraint: <Transition> animates a single child, not a list.

It triggers in three situations: a v-if, a v-show, or a component switch through <component :is>. To force a transition between two elements of the same type, give them different keys, as above.

Animating a list

For a list, you need <TransitionGroup>. Two differences: it accepts several children, and it renders a container element itself if you ask it to with tag.

In ReviewList.vue, replace the <ul>:

src/components/ReviewList.vue
<template>
  <div class="review-container">
    <h3>Avis</h3>
    <TransitionGroup name="liste" tag="ul">
      <li v-for="review in reviews" :key="review.id">
        {{ review.name }} a mis {{ review.rating }} étoiles
        <br>
        « {{ review.review }} »
      </li>
    </TransitionGroup>
  </div>
</template>

Each child needs a unique, stable key. The index will not do here: it changes with every insertion, and Vue could no longer tell which element is new. Let us give each review an identifier when it is added:

src/components/ProductDisplay.vue
<script setup>
function addReview(review) {
  reviews.value.push({ ...review, id: crypto.randomUUID() })
}
</script>

crypto.randomUUID() is available in every up-to-date browser, on pages served over HTTPS or from localhost.

The CSS adds one more class, -move, which animates the movement of the remaining elements when a neighbour disappears:

src/assets/boutique.css
.liste-enter-from,
.liste-leave-to {
  opacity: 0;
  transform: translateX(24px);
}

.liste-enter-active,
.liste-leave-active,
.liste-move {
  transition: all .3s ease;
}

Submit several reviews: each one slides in, and the others rearrange themselves smoothly.

Leaving and moving

For -move to work on removal, the leaving element must be taken out of the flow, otherwise the others only move once its animation has ended. Add position: absolute to the -leave-active class when your elements can be deleted.

Animating the first render

By default, nothing is animated on the initial render. The appear attribute changes that:

html
<Transition name="fade" appear>
  <p>Visible dès le chargement, en fondu</p>
</Transition>

CSS animations and hooks

transition is not the only option: a CSS animation works too, applied on the -enter-active class.

To drive a JavaScript animation library, <Transition> emits events at each step:

html
<Transition
  :css="false"
  @before-enter="avantEntree"
  @enter="entree"
  @after-enter="apresEntree"
  @leave="sortie"
>
  <p v-if="visible">Contenu</p>
</Transition>

With :css="false", Vue stops managing the classes and hands over to you. The enter and leave functions then receive the element and a done callback to call when the animation is over:

javascript
function entree(el, done) {
  // animation with the library of your choice
  done()
}

Motion is not always welcome

Some people disable animations at the system level, in particular to avoid motion sickness. Respect that setting:

src/assets/boutique.css
@media (prefers-reduced-motion: reduce) {
  .fade-enter-active,
  .fade-leave-active,
  .liste-enter-active,
  .liste-leave-active,
  .liste-move {
    transition: none;
  }
}

The interface stays functional, only the animation goes away.

This is the end of the tutorial

Your application displays a product and its variants, manages a cart, validates a form and animates its state changes. You have covered reactivity, directives, components, props, events and transitions: enough to read and write Vue every day.

Two directions for what comes next. Sharing state between distant components with Pinia, when bubbling events up becomes painful. And routing with Vue Router, as soon as the application has several pages.

In the Options API

<Transition> and <TransitionGroup> are built-in components: they are written exactly the same way. Only the JavaScript hook functions move, into the methods option.

ExerciseAdd a transition on the product image so that it fades when the variant changes. You will need a key bound to the selected variant.

Common errors

Using the Vue 2 classes .fade-enter and .fade-leave were renamed -enter-from and -leave-from. Nothing animates and no error shows up.
Forgetting mode="out-in" Without it, the entering element and the leaving element coexist for a split second and overlap.
Using the index as the key in a TransitionGroup Vue can no longer tell the new element from the others and animates the wrong entry.
Testing an animated component with Vue Test Utils Transitions are replaced by stubs by default. Pass global: { stubs: { 'transition-group': false } } to observe the real rendering.
Newsletter

New tests, tutorials and projects, by e-mail.

Reproducible tests, versioned code, dated results. Never any spam.