Enter and leave with Transition
Fade and slide an element in and out with the classes <Transition> adds, swap one element for another with a key and mode="out-in", and tone the motion down for people who ask for less.
What you'll learn
When you finish, you can do these.- 1Fade an element in and out with Transition and CSS
- 2Slide between tab panels with a key and out-in mode
- 3Keep motion smooth and respect the reduced-motion setting
- 4Spot and fix a note that pops in instead of fading
On a small online shop's page for a mug, a note appears when you add the mug to your basket: Saved to your basket. Shown with v-if, which adds an element only while a condition is true, the note pops in at once and vanishes as fast, so the eye misses it or flinches. The mug's tabs, Details and Reviews, jump the same way when one panel replaces the other.
You need one idea from Class and style bindings, where a to-do note changes its look: a CSS class is a name your stylesheet gives a style to. <Transition>, a wrapper built into Vue, adds such classes to one element at the right moments, and your CSS turns them into a fade or a slide. The Code tab holds a small version of that page: App.vue, with the tabs and a button that shows and hides the note, which has a file of its own, SavedNote.vue. By the end you can explain every line of their animation.
Six classes, one element
A fade needs a start, an end and the time between them, and <Transition> marks each with a class. It wraps exactly one element or component. When v-if adds it, Vue runs an enter. When v-if removes it, Vue runs a leave and keeps the element on the page until the leave ends. Each direction uses three classes.
v-enter-fromfirst framev-enter-activethe whole animationv-enter-tolast frame
v-leave-fromv-leave-activev-leave-tothen the element is removed
v-enter-from is the starting state. Vue adds it just before the element is inserted and removes it on the next frame, a split second later, when v-enter-to, the end state, takes over. v-enter-active stays for the whole enter, and it holds the CSS transition property: which properties to animate, and for how long. The leave mirrors them with v-leave-from, v-leave-active and v-leave-to.
A name replaces the v- prefix, so <Transition name="fade"> uses .fade-enter-from and the rest.
In App.vue, the note shows while saved is true. saved is a ref, a box that holds a value and tells Vue when it changes. The Show the note button flips it, and the note's Close button sends a close event up to App.vue, where @close sets it back to false.
In the template, the note is the tag <SavedNote>, named after its file, inside a <Transition> named fade:
<Transition name="fade">
<SavedNote v-if="saved" @close="saved = false" />
</Transition>
In the <style> block of App.vue, two rules make the fade:
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
The note's normal look is the visible end state, so -enter-to and -leave-from need no rule. Click Show the note and it fades in over 0.3 seconds; click Hide the note and it fades out.
Swap one element for another
A swap is one element leaving while another takes its place, like the panel under the mug's tabs, Details and Reviews, in App.vue. Clicking a tab sets a ref, tab, which starts as 'Details'. The panel, a <section>, takes tab as its :key, the name tag that told Vue which row of a list was which in the Directives lesson:
<Transition name="slide" mode="out-in" appear>
<section :key="tab" class="panel">
<h2>{{ tab }}</h2>
<p v-if="tab === 'Details'">Stoneware mug, 350 ml, safe in the dishwasher.</p>
<p v-else>4.8 out of 5 from 120 reviews.</p>
</section>
</Transition>
A new value of tab gives the panel a new name tag, so the old panel leaves and a new one enters. Without a key, Vue changes the same <section> in place, and nothing animates.
By default the leave and the enter run at the same time, so for a moment both panels sit in the page, one above the other. mode="out-in" waits for the old panel to finish leaving before the new one enters.
appear runs the enter on the first render too, which is why the Details panel slides in when the preview loads.
App.vue styles the slide with one rule for each class it needs:
.slide-enter-active { transition: opacity 0.25s ease-out, transform 0.25s ease-out; }
.slide-leave-active { transition: opacity 0.2s ease-in, transform 0.2s ease-in; }
.slide-enter-from { opacity: 0; transform: translateX(24px); }
.slide-leave-to { opacity: 0; transform: translateX(-24px); }
transform, the CSS property that moves an element, starts the new panel 24 pixels to the right and sends the old one 24 pixels to the left. Click Reviews: Details slides out, then Reviews slides in.
Cheap motion, and less of it
Motion should stay smooth on a slow phone, and gentle for people who ask for less of it.
Animate opacity and transform: the browser changes them without working out the page layout again. height, max-height and margin change the layout, so the browser works it out again on every frame, and on a slow phone the animation stutters.
Name the properties you animate in transition, as the slide's rules do: transition: all also animates anything else that changes, such as a width.
Some people ask their system for less motion, because large movement makes them dizzy or sick. The browser reports that choice as the media query prefers-reduced-motion: reduce, a condition your CSS can test, and Vue does not read it for you. At the end of the <style> block in App.vue, an @media rule tests it, keeps the fade and drops the movement:
@media (prefers-reduced-motion: reduce) {
.slide-enter-from, .slide-leave-to { transform: none; }
}
With the setting on, the panels fade in place without sliding.
What goes wrong
Two mistakes make the note pop in and out as if <Transition> were not there. In the first, nothing warns: the start and end classes are in the CSS, but no rule says the change should take time.
.fade-enter-from, .fade-leave-to { opacity: 0; }
Vue reads the element's transition while the classes are on. With no duration, it ends the enter at once and removes a leaving element on the next frame. The -active rule is what makes anything move.
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
The second mistake is a component with two root elements, side by side at the top of its template. Someone tidies SavedNote.vue and removes the wrapper div, which seems to do nothing. The note pops in again, and the preview warns Component inside <Transition> renders non-element root node that cannot be animated.
<template>
<p>Saved to your basket.</p>
<button @click="$emit('close')">Close</button>
</template>
Two roots make a fragment, with no one element around them, so Vue has nowhere to put the classes. A component inside <Transition> needs exactly one root element.
<template>
<div class="note">
<p>Saved to your basket.</p>
<button @click="$emit('close')">Close</button>
</div>
</template>
What to remember
<Transition>animates one element or component as it enters or leaves. It only adds classes; your CSS does the animating.- A changing
keyturns an update into a swap, andmode="out-in"lets the old element leave before the new one enters. - Animate
opacityandtransform, notheight. Vue ignoresprefers-reduced-motion: add the media query yourself and keep at most a fade. - Put
transitionon the-activeclasses, and give a component inside<Transition>one root element, or the element pops in.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- In App.vue, remove
mode="out-in"and click Reviews. For a moment both panels are on screen, the new one under the old, then the new one jumps up. Put it back, and the old panel leaves before the new one arrives. - Turn on reduced motion: in Chrome's DevTools, press Ctrl+Shift+P (Cmd+Shift+P on a Mac) and run "Emulate CSS prefers-reduced-motion: reduce", or turn on Reduce motion in a phone's accessibility settings. Switch tabs: the panels fade in place without sliding. Delete the
@mediablock and switch again: they slide. Put the block back. - Break it twice, undoing the first change before the second. Delete
.fade-enter-active,from the fade rule: the note pops in, while Hide the note still fades it out. Then, in SavedNote.vue, remove the<div class="note">wrapper and its closing tag: the note pops in without its background, and the preview warns about a non-element root node. Wrap it again and the fade returns.