---
title: "Enter and leave animations with <Transition> · vuejs.school"
description: "Vue's Transition component adds six classes while an element appears or disappears. Build a fade, a slide and an out-in swap, and learn the one-root rule."
canonical_url: "https://vuejs.school/community/blog/enter-and-leave-animations-with-transition"
meta:
  "og:description": "Vue's Transition component adds six classes while an element appears or disappears. Build a fade, a slide and an out-in swap, and learn the one-root rule."
  "og:image": "https://vuejs.school/_og/d/a_~RW50ZXIgYW5kIGxlYXZlIGFuaW1hdGlvbnMgd2l0aCA8VHJhbnNpdGlvbj4gwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~RW50ZXIgYW5kIGxlYXZlIGFuaW1hdGlvbnMgd2l0aCA8VHJhbnNpdGlvbj4%2Cdescription_~VnVlJ3MgVHJhbnNpdGlvbiBjb21wb25lbnQgYWRkcyBzaXggY2xhc3NlcyB3aGlsZSBhbiBlbGVtZW50IGFwcGVhcnMgb3IgZGlzYXBwZWFycy4gQnVpbGQgYSBmYWRlLCBhIHNsaWRlIGFuZCBhbiBvdXQtaW4gc3dhcCwgYW5kIGxlYXJuIHRoZSBvbmUtcm9vdCBydWxlLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbnRlci1hbmQtbGVhdmUtYW5pbWF0aW9ucy13aXRoLXRyYW5zaXRpb24i%2Cs_mUb5VhG148cJ1n4B.png"
  "og:image:alt": "Enter and leave animations with <Transition> · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Enter and leave animations with <Transition> · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~RW50ZXIgYW5kIGxlYXZlIGFuaW1hdGlvbnMgd2l0aCA8VHJhbnNpdGlvbj4gwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~RW50ZXIgYW5kIGxlYXZlIGFuaW1hdGlvbnMgd2l0aCA8VHJhbnNpdGlvbj4%2Cdescription_~VnVlJ3MgVHJhbnNpdGlvbiBjb21wb25lbnQgYWRkcyBzaXggY2xhc3NlcyB3aGlsZSBhbiBlbGVtZW50IGFwcGVhcnMgb3IgZGlzYXBwZWFycy4gQnVpbGQgYSBmYWRlLCBhIHNsaWRlIGFuZCBhbiBvdXQtaW4gc3dhcCwgYW5kIGxlYXJuIHRoZSBvbmUtcm9vdCBydWxlLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbnRlci1hbmQtbGVhdmUtYW5pbWF0aW9ucy13aXRoLXRyYW5zaXRpb24i%2Cs_mUb5VhG148cJ1n4B.png"
  "twitter:image:alt": "Enter and leave animations with <Transition> · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Fade an element in and out

`<Transition>` is a built-in component, so you use it without importing anything. Put it around the element you toggle. This component shows a notice when you press a button:

*src/components/Notice.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
  <button type="button" @click="open = !open">Toggle notice</button>
  <Transition name="fade">
    <p v-if="open" class="notice">Saved. You can close this tab.</p>
  </Transition>
</template>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
```

Without the wrapper, `v-if` inserts and removes the `<p>` in one frame. With it, Vue puts the `fade-` classes from the `<style>` block on the `<p>` while it enters and leaves. The element starts at `opacity: 0`, and the `transition` rule carries it to its normal `opacity: 1` over 0.2 seconds. Leaving runs in reverse, and Vue removes the `<p>` from the DOM only once the transition has ended.

`v-show` works the same way. The one difference is that after leaving, the element stays in the DOM with `display: none`.

## What the six classes do

Vue adds classes in two groups of three. Their prefix comes from the `name` prop: with `name="fade"` they start with `fade-`, as in the example above, and with no `name` they start with `v-`. One component can hold several transitions with different names, and they do not interfere with each other.

When the element enters, Vue:

1. Adds `v-enter-from` and `v-enter-active`, then inserts the element.
2. One frame later, removes `v-enter-from` and adds `v-enter-to`.
3. When the transition ends, removes `v-enter-active` and `v-enter-to`.

Leaving follows the same pattern with `v-leave-from`, `v-leave-active` and `v-leave-to`, and ends with Vue removing the element.

So put the `transition` property in the `-active` classes, because they are present for the whole phase, and put the start and end states in `-from` and `-to`. A fade only needs `-enter-from` and `-leave-to`, since the resting state is the element's normal style.

To know when the transition has ended, Vue reads the element's computed style, looks for a `transition-duration` or an `animation-duration`, and listens for `transitionend` or `animationend`. If it finds neither, it removes the classes on the next frame, so an element with no matching CSS simply appears. If you use both a transition and an animation on the same element, tell Vue which one to wait for with `type="animation"` or `type="transition"`.

## Slide in with CSS keyframes

A CSS transition moves one property from one value to another: opacity 0 to 1, or a transform from `translateY(12px)` back to nothing. That covers most interface motion. A CSS animation with `@keyframes` can have more than two steps, for example an overshoot that goes past the end point and settles back. For keyframes you put the `animation` property in the `-active` classes and skip `-from` and `-to`. This slide goes in a global stylesheet, so any component can use it:

*src/assets/transitions.css*

```
.slide-enter-active {
  animation: slide-in 0.25s ease-out;
}
.slide-leave-active {
  animation: slide-in 0.2s ease-in reverse;
}
@keyframes slide-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
```

Import the file once in `src/main.ts` (`import './assets/transitions.css'`), then use it with `<Transition name="slide">`. The leave rule plays the same keyframes backwards, which keeps the two directions symmetric and saves a second `@keyframes` block.

Whichever you choose, animate `opacity` and `transform` only. Those two properties do not trigger layout, so they stay smooth on slow phones. Animating `height`, `margin` or `top` makes the browser recalculate the layout on every frame.

## Swap two elements with mode

A common need is to replace one element with another: a "Save" button becomes a "Saving" label, or tab A becomes tab B. With `v-if` and `v-else` inside one `<Transition>`, both elements animate at the same time by default. The new one enters while the old one leaves, so for a moment both are on screen and the layout jumps.

The `mode` prop fixes it:

*src/components/SaveButton.vue*

```
<script setup lang="ts">
const props = defineProps<{ saving: boolean }>()
const emit = defineEmits<{ save: [] }>()
</script>

<template>
  <Transition name="fade" mode="out-in">
    <span v-if="props.saving" key="saving">Saving</span>
    <button v-else key="idle" type="button" @click="emit('save')">Save</button>
  </Transition>
</template>
```

`mode="out-in"` waits for the old element to finish leaving before the new one starts entering. The opposite, `mode="in-out"`, is rarely what you want; out-in is the one to reach for.

The two `key` attributes are optional here: the template compiler already gives each `v-if`/`v-else` branch a key of its own, so Vue always sees two different elements. You need a `key` yourself when a single element stays and only its content changes. `<span :key="label">{{ label }}</span>` makes Vue replace the `<span>` each time `label` changes, which runs the leave and the enter. Without the key, Vue updates the text in place and nothing animates.

`SaveButton.vue` has no fade rules of its own, and the ones in `Notice.vue` are scoped to that component. Copy that `<style scoped>` block into `SaveButton.vue`, or move the fade rules into `transitions.css`; without them the two elements swap with no fade.

## Animate on first render with appear

By default the enter transition runs only when the element appears after the initial render. A panel that is visible from the start just shows up. If you also want it to animate on page load, add the `appear` prop:

```
<Transition name="fade" appear>
  <section class="welcome">Welcome back</section>
</Transition>
```

Use it sparingly. One element animating in on load is a nice touch; a page where ten things fade in is slow to read.

## Animate from JavaScript with hooks

Sometimes CSS is not enough. You want a spring curve, or you are using an animation library that drives styles from JavaScript. `<Transition>` exposes the same phases as events: `before-enter`, `enter`, `after-enter`, `enter-cancelled`, and the four `leave` equivalents. Each hook receives the element, and `enter` and `leave` also receive a `done` callback you must call when the animation finishes.

*src/components/Popover.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)

function onEnter(el: Element, done: () => void) {
  const animation = el.animate(
    [{ opacity: 0, transform: 'scale(0.95)' }, { opacity: 1, transform: 'scale(1)' }],
    { duration: 180, easing: 'ease-out' },
  )
  animation.onfinish = () => done()
}

function onLeave(el: Element, done: () => void) {
  const animation = el.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 120 })
  animation.onfinish = () => done()
}
</script>

<template>
  <button type="button" @click="open = !open">Menu</button>
  <Transition :css="false" @enter="onEnter" @leave="onLeave">
    <div v-if="open" class="popover">Menu items</div>
  </Transition>
</template>
```

This example uses the browser's own Web Animations API, `el.animate()`, so it needs no library at all. The important detail is `:css="false"`. It tells Vue to skip the CSS detection and the six classes entirely, so the hooks are the only thing driving the animation and no stray stylesheet rule can interfere. Because the hooks declare a `done` argument, Vue waits for you to call it before it removes the element.

If you prefer a class-based library such as Animate.css, you do not need hooks. Pass the library's class names through `enter-active-class` and `leave-active-class`, and Vue applies them instead of the generated ones.

## Turn animations off for reduced motion

Some people turn on "reduce motion" in their operating system because animation makes them dizzy or distracts them. Browsers expose that setting as the `prefers-reduced-motion` media query. Add this once, in a global stylesheet:

*src/assets/main.css*

```
@media (prefers-reduced-motion: reduce) {
  .fade-enter-active,
  .fade-leave-active,
  .slide-enter-active,
  .slide-leave-active {
    transition: none !important;
    animation: none !important;
  }
}
```

`!important` makes the rule win: `Notice.vue`'s styles are `scoped`, so Vue compiles its `.fade-enter-active` to `.fade-enter-active[data-v-…]`, which is more specific than the plain class here. Without `!important` the fade keeps running, and the slide stops only if `main.css` happens to load after `transitions.css`.

With no transition or animation to detect, Vue removes the classes on the next frame and the element appears at once. Nothing in your component changes, so you keep one code path. For JavaScript hooks, check `window.matchMedia('(prefers-reduced-motion: reduce)').matches` at the top of the hook and call `done()` straight away when it is true.

## Keep one element inside `<Transition>`

`<Transition>` animates one element. A toast that puts two siblings inside it fails:

*src/components/Toast.vue**Breaks***

```
<Transition name="fade">
  <strong v-if="open">Saved</strong>
  <p v-if="open">Your changes are safe.</p>
</Transition>
```

In development the template compiler stops with `<Transition> expects exactly one child element or component.`, and Vite reports it as an error. Code that skips that check, such as a render function, renders only the first element, with the console warning `<transition> can only be used on a single element or component. Use <transition-group> for lists.` The fix is a single wrapper:

*src/components/Toast.vue**Works***

```
<Transition name="fade">
  <div v-if="open" class="toast">
    <strong>Saved</strong>
    <p>Your changes are safe.</p>
  </div>
</Transition>
```

The same rule bites more quietly when the child is a component. A component whose template has several root nodes cannot be transitioned either, because Vue has no single element to attach the classes to. The page still works and the component just appears; the only sign is a development warning, `Component inside <Transition> renders non-element root node that cannot be animated.` Give the component one root element and it works. Lists that add and remove items are a different job; that is what `<TransitionGroup>` is for, and it deserves its own post.

**Check it stuck**3 questions from the Enter and leave with Transition quiz.

[**Practice now**](https://vuejs.school/learn/animation/enter-and-leave-with-transition?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 7 min read**Page transitions with the View Transitions API and motion-v**](https://vuejs.school/community/blog/page-transitions-with-the-view-transitions-api-and-motion-v) [Release notes · 6 min read**Motion for Vue 2.0: what changed**](https://vuejs.school/community/blog/motion-for-vue-2-what-changed)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/enter-and-leave-animations-with-transition#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-04-24","description":"Vue's Transition component adds six classes while an element appears or disappears. Build a fade, a slide and an out-in swap, and learn the one-root rule.","name":"Enter and leave animations with \u003CTransition>","url":"https://vuejs.school/community/blog/enter-and-leave-animations-with-transition","breadcrumb":{"@id":"https://vuejs.school/community/blog/enter-and-leave-animations-with-transition#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/enter-and-leave-animations-with-transition"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/enter-and-leave-animations-with-transition#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-04-24","description":"Vue's Transition component adds six classes while an element appears or disappears. Build a fade, a slide and an out-in swap, and learn the one-root rule.","headline":"Enter and leave animations with \u003CTransition>","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~RW50ZXIgYW5kIGxlYXZlIGFuaW1hdGlvbnMgd2l0aCA8VHJhbnNpdGlvbj4gwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~RW50ZXIgYW5kIGxlYXZlIGFuaW1hdGlvbnMgd2l0aCA8VHJhbnNpdGlvbj4%2Cdescription_~VnVlJ3MgVHJhbnNpdGlvbiBjb21wb25lbnQgYWRkcyBzaXggY2xhc3NlcyB3aGlsZSBhbiBlbGVtZW50IGFwcGVhcnMgb3IgZGlzYXBwZWFycy4gQnVpbGQgYSBmYWRlLCBhIHNsaWRlIGFuZCBhbiBvdXQtaW4gc3dhcCwgYW5kIGxlYXJuIHRoZSBvbmUtcm9vdCBydWxlLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbnRlci1hbmQtbGVhdmUtYW5pbWF0aW9ucy13aXRoLXRyYW5zaXRpb24i%2Cs_mUb5VhG148cJ1n4B.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/enter-and-leave-animations-with-transition#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/enter-and-leave-animations-with-transition#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/enter-and-leave-animations-with-transition#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Enter and leave animations with \u003CTransition>","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~RW50ZXIgYW5kIGxlYXZlIGFuaW1hdGlvbnMgd2l0aCA8VHJhbnNpdGlvbj4gwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~RW50ZXIgYW5kIGxlYXZlIGFuaW1hdGlvbnMgd2l0aCA8VHJhbnNpdGlvbj4%2Cdescription_~VnVlJ3MgVHJhbnNpdGlvbiBjb21wb25lbnQgYWRkcyBzaXggY2xhc3NlcyB3aGlsZSBhbiBlbGVtZW50IGFwcGVhcnMgb3IgZGlzYXBwZWFycy4gQnVpbGQgYSBmYWRlLCBhIHNsaWRlIGFuZCBhbiBvdXQtaW4gc3dhcCwgYW5kIGxlYXJuIHRoZSBvbmUtcm9vdCBydWxlLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbnRlci1hbmQtbGVhdmUtYW5pbWF0aW9ucy13aXRoLXRyYW5zaXRpb24i%2Cs_mUb5VhG148cJ1n4B.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~RW50ZXIgYW5kIGxlYXZlIGFuaW1hdGlvbnMgd2l0aCA8VHJhbnNpdGlvbj4gwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~RW50ZXIgYW5kIGxlYXZlIGFuaW1hdGlvbnMgd2l0aCA8VHJhbnNpdGlvbj4%2Cdescription_~VnVlJ3MgVHJhbnNpdGlvbiBjb21wb25lbnQgYWRkcyBzaXggY2xhc3NlcyB3aGlsZSBhbiBlbGVtZW50IGFwcGVhcnMgb3IgZGlzYXBwZWFycy4gQnVpbGQgYSBmYWRlLCBhIHNsaWRlIGFuZCBhbiBvdXQtaW4gc3dhcCwgYW5kIGxlYXJuIHRoZSBvbmUtcm9vdCBydWxlLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbnRlci1hbmQtbGVhdmUtYW5pbWF0aW9ucy13aXRoLXRyYW5zaXRpb24i%2Cs_mUb5VhG148cJ1n4B.png"}]}
```