Transitions and Animation

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

<Transition> and <TransitionGroup> are built-in components that apply enter, leave, and move animations when content is added, removed, or reordered. They add and remove a small set of CSS classes around the DOM change; you supply the actual animation in CSS, or take over completely with JavaScript hooks.

<Transition>

Wrap a single element or component and Vue animates it whenever it is toggled by v-if, shown by v-show, or swapped through a dynamic :is:

<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>

<template>
  <button @click="show = !show">toggle</button>
  <Transition>
    <p v-if="show">Hello</p>
  </Transition>
</template>

<style>
.v-enter-active,
.v-leave-active {
  transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
  opacity: 0;
}
</style>

The six classes

Timeline of the six transition classes: v-enter-from with v-enter-active at the first frame, v-enter-active across the transition, v-enter-active with v-enter-to at the end, and the matching three classes for v-leave
Class Applied

v-enter-from

starting state of enter; added before insert, removed one frame after

v-enter-active

present for the whole enter; declare transition / animation and duration here

v-enter-to

ending state of enter; added one frame after insert, removed when the transition ends

v-leave-from

starting state of leave; added the instant a leave is triggered

v-leave-active

present for the whole leave

v-leave-to

ending state of leave; added one frame after leave starts

The v- prefix is the default. With name, it is replaced: <Transition name="fade"> produces fade-enter-from, fade-enter-active, and so on.

Custom class names

Provide each class explicitly — required to plug in an external animation library such as Animate.css:

<Transition
  enter-active-class="animate__animated animate__fadeInDown"
  leave-active-class="animate__animated animate__fadeOutUp"
>
  <p v-if="show">Hello</p>
</Transition>

mode

By default the entering and leaving elements are present at the same time. mode="out-in" waits for the old element to finish leaving before the new one enters (the usual choice for swaps); mode="in-out" is the reverse:

<Transition name="slide" mode="out-in">
  <component :is="currentTab" />
</Transition>

appear

Add appear to also play the enter transition on the component’s initial render:

<Transition appear>
  <p>fades in on first paint</p>
</Transition>

Between elements and dynamic components

One <Transition> can animate a swap between two elements as long as only one is shown at a time; give each a distinct key when they use the same tag. Dynamic components via :is behave the same way:

<Transition name="slide" mode="out-in">
  <span :key="status">{{ status }}</span>
</Transition>

JavaScript hooks

For spring physics or anything CSS cannot express, listen to the lifecycle events and disable CSS class detection with :css="false":

<script setup>
function onEnter(el, done) {
  el.style.opacity = 0
  requestAnimationFrame(() => {
    el.style.transition = 'opacity 0.4s ease'
    el.style.opacity = 1
  })
  el.addEventListener('transitionend', done, { once: true })
}
function onLeave(el, done) {
  el.style.opacity = 0
  el.addEventListener('transitionend', done, { once: true })
}
</script>

<template>
  <Transition :css="false" @enter="onEnter" @leave="onLeave">
    <p v-if="show">Hello</p>
  </Transition>
</template>

The full set is @before-enter, @enter, @after-enter, @enter-cancelled, and the matching @before-leave, @leave, @after-leave, @leave-cancelled. With :css="false", calling the done callback in @enter / @leave is what tells Vue the animation has finished.

<TransitionGroup>

Animates insertion, removal, and reordering of a list. Unlike <Transition> it can render a real wrapper element (pass tag), ignores mode, and requires a key on every child:

<script setup>
import { ref } from 'vue'
const items = ref([1, 2, 3, 4, 5])
function shuffle() {
  items.value = [...items.value].sort(() => Math.random() - 0.5)
}
</script>

<template>
  <button @click="shuffle">shuffle</button>
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item">{{ item }}</li>
  </TransitionGroup>
</template>
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
.list-enter-active,
.list-leave-active {
  transition: all 0.4s ease;
}
/* items gliding to a new position */
.list-move {
  transition: transform 0.4s ease;
}
/* pull leaving items out of flow so siblings can move into the gap */
.list-leave-active {
  position: absolute;
}

Move transitions (FLIP)

The v-move class — list-move above — is applied to items whose position changed. Vue animates them with the FLIP technique: it records each item’s old and new box, applies an inverting transform so the item appears not to have moved, then transitions that transform to zero. Give leaving items position: absolute so the remaining items can animate into the freed space.

Staggering

Drive a per-item delay from a data attribute inside a JavaScript hook, using a timeline library such as GSAP:

<script setup>
import gsap from 'gsap'
function onEnter(el, done) {
  gsap.to(el, {
    opacity: 1,
    y: 0,
    delay: el.dataset.index * 0.05,
    onComplete: done
  })
}
</script>

<template>
  <TransitionGroup :css="false" tag="ul" @enter="onEnter">
    <li
      v-for="(item, i) in items"
      :key="item.id"
      :data-index="i"
      style="opacity: 0"
    >{{ item.text }}</li>
  </TransitionGroup>
</template>

Animation without the transition components

The transition components only help around insert, remove, and reorder. Everything else is plain reactivity. Animation Techniques covers four approaches:

Class and style bindings. Bind a class or an inline style to state and let CSS transition it:

<template>
  <div
    class="box"
    :class="{ active }"
    :style="{ transform: `translateX(${x}px)` }"
  />
</template>

<style>
.box { transition: transform 0.3s ease, background-color 0.3s ease; }
.box.active { background-color: teal; }
</style>

Watchers driving imperative animation. Watch a value and call an animation library in the callback:

import { ref, watch } from 'vue'
import gsap from 'gsap'

const count = ref(0)
watch(count, (n, old) => {
  gsap.fromTo('.counter',
    { innerText: old },
    { innerText: n, duration: 0.5, snap: { innerText: 1 } }
  )
})

State-driven tweening. Keep a separate "displayed" ref, animate it toward the real value, and render the displayed one — the standard approach for animated numbers and morphing SVG paths.

GSAP and friends. The guide uses GSAP; the same shape works with the Web Animations API or @vueuse/motion. Reach for these when CSS transitions cannot express the motion — spring physics, path morphing, sequenced timelines.

The styles above are plain CSS; with a utility framework the class bindings are written the same way — see Tailwind Reference.

Page and route transitions

Wrap <RouterView> in a <Transition> using the view’s slot form, so one wrapper covers every route component:

<template>
  <RouterView v-slot="{ Component, route }">
    <Transition :name="route.meta.transition || 'fade'" mode="out-in">
      <component :is="Component" :key="route.path" />
    </Transition>
  </RouterView>
</template>

Per-route names come from each route’s meta. See Routing for meta and the router setup, and Route Transitions for the scroll-position and nested-route caveats.

See also

  • KeepAlive, Teleport, and Suspense — <Transition> composed with <KeepAlive> for cached tab views, and teleported modals that animate in and out.

  • Routing — route meta, named views, and navigation guards.

  • Tailwind Reference — utility classes for the state-driven :class bindings shown here.