Transitions

<Transition> adds six well-timed classes around an element that enters or leaves; you write the CSS.

Syntax<transition name="fade" mode="out-in" appear> <!-- one child --> </transition>

<Transition> animates a single element or component as it enters or leaves the DOM. Written <transition> in a template it is the same built-in component. It does not animate anything by itself: it adds and removes classes at the right moments, and you supply the CSS.

The six classes

  • v-enter-from — applied before insertion, removed one frame after.
  • v-enter-active — applied for the whole enter; this is where duration, delay and easing go.
  • v-enter-to — added one frame after insertion, removed when the transition ends.
  • v-leave-from, v-leave-active, v-leave-to — the same three, mirrored for leaving.

The v- prefix is the default. With name="fade" the classes become fade-enter-from and so on, which is why most transitions are written as a name plus a few rules.

Triggers and props

The trigger is the child appearing or disappearing: v-if, v-show, a dynamic component, or a changed key. mode="out-in" makes the old child finish leaving before the new one enters, which is normally what you want when one replaces another. appear runs the enter transition on first render. duration takes a number or { enter: 500, leave: 800 } when Vue cannot detect the end itself. The enter-active-class family replaces the generated names with class names from an animation library.

What trips people up

Exactly one child element is allowed, and two alternative v-if branches inside one transition need distinct key attributes to be treated as different elements.

Example

Example · vue
<!-- ShippingNote.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)

function toggle(): void {
  open.value = !open.value
}
</script>

<template>
  <!-- @click is the shorthand for v-on:click -->
  <button type="button" class="toggle" @click="toggle">
    {{ open ? 'Hide' : 'Show' }} shipping details
  </button>

  <transition name="fade">
    <p v-if="open" class="panel">Orders ship within two to three working days.</p>
  </transition>
</template>

<style scoped>
.toggle {
  font: inherit;
}

.panel {
  margin: 0.5rem 0 0;
  padding: 0.75rem;
  background: #f3f5f8;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 200ms ease, transform 200ms ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
  transform: translateY(-4px);
}
</style>

When to use it

  • Fade in a details panel or an inline error instead of having it appear abruptly.
  • Cross-fade between two tab panels without the outgoing one overlapping the incoming one.
  • Apply a short fade to route changes so navigation does not flash.

More examples

Animating a route change

RouterView's slot hands you the matched component so you can wrap it yourself. mode="out-in" avoids two pages overlapping, the key on <component> stops Vue reusing one instance across routes, and a route can opt into a different name with meta: { transition: 'slide-left' }.

Example · vue
<!-- App.vue -->
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>

<template>
  <router-view v-slot="{ Component, route }">
    <transition
      :name="(route.meta.transition as string | undefined) ?? 'page'"
      mode="out-in"
    >
      <component :is="Component" :key="route.path" />
    </transition>
  </router-view>
</template>

<style>
/* not scoped: these classes land on each route component's own root */
.page-enter-active,
.page-leave-active {
  transition: opacity 120ms ease;
}

.page-enter-from,
.page-leave-to {
  opacity: 0;
}
</style>

Swapping two panels with a key

Two mutually exclusive branches in one transition. The keys are what make Vue treat them as different elements rather than patching one section in place, and out-in keeps the two panels from sitting on top of each other mid-swap.

Example · vue
<!-- AccountTabs.vue -->
<script setup lang="ts">
import { ref } from 'vue'

type Tab = 'profile' | 'billing'

const tab = ref<Tab>('profile')
</script>

<template>
  <div class="tabs">
    <button type="button" @click="tab = 'profile'">Profile</button>
    <button type="button" @click="tab = 'billing'">Billing</button>
  </div>

  <transition name="slide" mode="out-in">
    <section v-if="tab === 'profile'" key="profile" class="panel">Your name and email.</section>
    <section v-else key="billing" class="panel">Card on file and invoices.</section>
  </transition>
</template>

<style scoped>
.panel {
  padding: 0.75rem 0;
}

.slide-enter-active,
.slide-leave-active {
  transition: opacity 150ms ease, transform 150ms ease;
}

.slide-enter-from {
  opacity: 0;
  transform: translateX(8px);
}

.slide-leave-to {
  opacity: 0;
  transform: translateX(-8px);
}
</style>

Custom classes and appear

When an animation library supplies the class names, the enter-active-class and leave-active-class props replace the generated ones. appear runs the enter transition on first render, which a toast needs because it is already present when mounted, and the explicit duration tells Vue when to finish rather than relying on detection.

Example · vue
<!-- ToastMessage.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const props = defineProps<{
  text: string
}>()

const visible = ref(true)

function dismiss(): void {
  visible.value = false
}
</script>

<template>
  <transition
    appear
    :duration="{ enter: 220, leave: 160 }"
    enter-active-class="animate__animated animate__fadeInDown"
    leave-active-class="animate__animated animate__fadeOutUp"
  >
    <div v-if="visible" class="toast" role="status">
      {{ props.text }}
      <button type="button" @click="dismiss">Dismiss</button>
    </div>
  </transition>
</template>

<style scoped>
.toast {
  display: flex;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  background: #222;
  color: #fff;
  border-radius: 4px;
}
</style>

Discussion

  • Be the first to comment on this lesson.