Transition

Vue's built-in Transition component: the six classes applied around enter and leave, naming them, JavaScript hooks, and appear.

Syntax<transition name="fade" mode="out-in" appear><p v-if="shown">text</p></transition>

<Transition> is a built-in component. It wraps one element or component and applies CSS classes while that element is being inserted or removed, so there is something to animate between "present" and "gone". Nothing registers it; it is available in every template. <Transition> and <transition> are the same component written two ways.

It acts on a child that actually enters or leaves: one controlled by v-if or v-show, a dynamic component, or an element whose key changed.

The six classes

ClassWhen it is on the element
v-enter-fromThe start state, removed one frame after insertion
v-enter-activeThe whole entering phase; put the transition here
v-enter-toThe end state, added one frame after insertion
v-leave-fromThe start state, added when leaving begins
v-leave-activeThe whole leaving phase
v-leave-toThe end state, until the transition finishes

The name prop changes the v- prefix: name="fade" gives fade-enter-from and the rest. name is shorthand for the six class props written out — enter-from-class, enter-active-class, enter-to-class, leave-from-class, leave-active-class, leave-to-class — which you use when the class names come from elsewhere, such as a utility framework.

JavaScript instead of CSS

The hooks are @before-enter, @enter, @after-enter, @enter-cancelled and the matching four for leave. @enter and @leave receive (el, done). Add :css="false" and Vue stops inspecting CSS; done() is then required, and the element waits in the DOM until you call it. @enter is shorthand for v-on:enter.

What trips people up

Only one element may be rendered at a time, so a v-if / v-else / v-else-if chain is fine inside a single <Transition> — only one branch ever exists. When the branches use the same tag name, give each a unique key, or Vue patches the one element instead of transitioning between them. appear animates the initial render only. By default enter and leave run at the same time, which overlaps two panels; mode="out-in" finishes the leave first.

Example

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

const open = ref(true)

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

<template>
  <button type="button" @click="toggle">
    {{ open ? 'Hide details' : 'Show details' }}
  </button>

  <transition name="fade" appear>
    <p v-if="open" class="details">Shipping takes three to five days.</p>
  </transition>
</template>

<style scoped>
.details {
  margin-top: 0.5rem;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 180ms ease-out;
}

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

When to use it

  • Fade a validation message in when a field becomes invalid and out when it is fixed.
  • Cross-fade two tab panels with mode="out-in" so the layout does not jump.
  • Slide a cart drawer in and out without pulling in an animation library.
  • Hand a leave animation to the Web Animations API when CSS cannot express it.

More examples

Longhand class names and out-in mode

The same transition as the main example, written with the six class props instead of name="fade" — the form to reach for when the class names come from a CSS framework rather than from you. The :key on the paragraph is what makes a tab change count as a leave and an enter; mode="out-in" means the old text has gone before the new text arrives, so the line does not double up.

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

type TabId = 'overview' | 'specs'

const tab = ref<TabId>('overview')
</script>

<template>
  <button type="button" @click="tab = 'overview'">Overview</button>
  <button type="button" @click="tab = 'specs'">Specs</button>

  <transition
    mode="out-in"
    enter-from-class="panel-hidden"
    enter-active-class="panel-moving"
    enter-to-class="panel-shown"
    leave-from-class="panel-shown"
    leave-active-class="panel-moving"
    leave-to-class="panel-hidden"
  >
    <p :key="tab">
      {{ tab === 'overview' ? 'A small desk lamp.' : '12 W, 2700 K.' }}
    </p>
  </transition>
</template>

<style scoped>
.panel-moving {
  transition: opacity 150ms ease;
}

.panel-hidden {
  opacity: 0;
}

.panel-shown {
  opacity: 1;
}
</style>

JavaScript hooks with the Web Animations API

With :css="false" Vue applies no classes and does not wait for a CSS transition; it waits for done(). Reach for this when the animation needs values you only know at runtime, or when you are driving an animation library. Forgetting to call done() in @leave leaves the element in the DOM for good.

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

const saved = ref(false)

function onEnter(el: Element, done: () => void): void {
  const animation = el.animate(
    [
      { opacity: 0, transform: 'translateY(8px)' },
      { opacity: 1, transform: 'none' }
    ],
    { duration: 200, easing: 'ease-out' }
  )
  animation.onfinish = () => done()
}

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

<template>
  <button type="button" @click="saved = !saved">Save</button>

  <transition :css="false" @enter="onEnter" @leave="onLeave">
    <p v-if="saved" role="status">Saved</p>
  </transition>
</template>

Telling Vue the duration of a nested transition

Vue reads the transition duration off the root element to decide when the enter or leave is finished. Here the root fades in 150ms while a child slides for 400ms, so Vue would call it done too early and strip the classes mid-slide. :duration states the real figures; it takes a single number or an object with enter and leave.

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

const open = ref(false)
</script>

<template>
  <button type="button" @click="open = !open">Toggle receipt</button>

  <transition name="outer" :duration="{ enter: 400, leave: 250 }">
    <div v-if="open" class="card">
      <p class="inner">Order #1042</p>
    </div>
  </transition>
</template>

<style scoped>
.card {
  padding: 1rem;
}

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

.outer-enter-from,
.outer-leave-to {
  opacity: 0;
}

.outer-enter-active .inner {
  transition: transform 400ms ease-out;
}

.outer-enter-from .inner {
  transform: translateY(12px);
}
</style>

Discussion

  • Be the first to comment on this lesson.