Motion for Vue and auto-animate
Describe an end state and let Motion for Vue animate to it with springs and gestures, keep a leaving element on screen with AnimatePresence, and animate a list with one auto-animate directive and no CSS.
What you'll learn
When you finish, you can do these.- 1Make a card spring in and grow on hover with Motion for Vue
- 2Fade a card out as it leaves, with AnimatePresence
- 3Animate a list with one auto-animate directive and no CSS
- 4Choose CSS, auto-animate or Motion for the motion you need
A small online shop's page shows a card for a stoneware mug, a button that hides it, and a Saved for later list. Fading the card in takes a CSS transition, a few lines that spread a style change over a set time. Some motion is much harder to write that way: a card that springs into place and settles, a card that shrinks under your finger, rows that glide to new places when you save one.
Two libraries cover those cases: Motion for Vue, the motion-v package, and @formkit/auto-animate. You need one idea from Animating lists with TransitionGroup, where a basket's rows slide into place: each row needs a key that tells Vue which row is which, such as an id, never its position. The Code tab holds the shop page, App.vue, animated with both; by the end you can tune its motion.
Describe the end state, with a tween or a spring
Motion for Vue animates an element between states you describe. App.vue imports motion from motion-v; motion.article, motion.div and the rest are normal HTML elements with animation props. The card is a motion.article:
<motion.article
:initial="{ opacity: 0, y: 16 }"
:animate="{ opacity: 1, y: 0 }"
:transition="transition"
:while-hover="{ scale: 1.03 }"
:while-press="{ scale: 0.97 }"
>
:initial is the state the card starts in and :animate the state it goes to. Motion animates between them when the card appears and whenever :animate changes. y: 16 draws the card 16 pixels lower, so it rises as it fades in.
Gestures are props too: :while-hover makes the card slightly larger while the pointer is over it, and :while-press slightly smaller while it is held down. When the gesture ends, Motion returns to :animate.
:transition says how the card moves. In the script block of App.vue, transition holds a tween, timed like a CSS transition: a duration in seconds and an ease, the curve its speed follows.
const transition = { duration: 0.3, ease: 'easeOut' }
The card rises in 0.3 seconds, fast at first, then slower.
A spring is a physical model instead of a clock: stiffness is how hard it pulls towards the target, damping how quickly it stops moving. The comment above the tween, marked Try it 1, holds one:
const transition = { type: 'spring', stiffness: 300, damping: 18 }
With this spring, the card overshoots its resting place and settles. A spring also copes with interruption: it keeps the speed the card already had, so a card you hover and leave quickly turns around smoothly, where a tween stops dead and starts again.
Exit needs AnimatePresence
This section makes the card animate out as well as in. In App.vue, the card has v-if="show": it is on the page only while the ref show is true. The Hide the card button sets show to false, and the card disappears at once.
Vue's own <Transition> wrapper keeps a leaving element on the page until its CSS transition ends; Motion's version is <AnimatePresence>. Wrap the card in it and give the card a key, so the wrapper can tell which child left. Then :exit, the state the card leaves in, plays before the card goes:
<AnimatePresence>
<motion.article v-if="show" key="card" :exit="{ opacity: 0, y: -16 }">
</AnimatePresence>
Click Hide the card: it fades out as it rises, then it is gone. The card must be a direct child of the wrapper.
When two elements swap, such as a card and an empty message, mode="wait" on <AnimatePresence> holds the new one until the old one has left, like mode="out-in" on <Transition>.
A list with no CSS at all
@formkit/auto-animate, auto-animate for short, animates a list with no CSS: all it takes is one directive, an attribute that starts with v-.
In App.vue, the Saved for later list shows the ref saved: Mug and Teapot at first, and Save a bowl adds a bowl at the top. Import the directive and put it on the <ul>, the parent of the rows (their Remove buttons are left out here):
<script setup lang="ts">
import { vAutoAnimate } from '@formkit/auto-animate'
</script>
<template>
<ul v-auto-animate class="list">
<li v-for="item in saved" :key="item.id">{{ item.name }}</li>
</ul>
</template>
Click Save a bowl: Bowl 3 fades in at the top while Mug and Teapot glide down.
The import is all the setup: in <script setup>, a name that starts with v and a capital letter is a directive for that file, so vAutoAnimate is v-auto-animate in the template.
The directive watches the parent for changes in the DOM, the browser's model of the page, and slides each moved child from its old place to its new one: FLIP, the trick Vue's <TransitionGroup> uses too. Only the immediate children animate, so the directive goes on the <ul>, not on a wrapper around it.
Which tool, when
Each tool fits a different kind of motion.
motion-vStates on each element, with springs, gestures, drag and exits through AnimatePresence. You pick the physics, one prop per behaviour.
@formkit/auto-animateOne directive on a parent. Added, removed and moved children animate with FLIP. No CSS, no props on each child, no control beyond duration and easing.
Reach for CSS first: Vue's <Transition> and <TransitionGroup> cost nothing to download. Take auto-animate when a list or a grid rearranges itself and the exact curve does not matter. Take Motion when the motion is the point: a spring, a gesture, a drag, anything that must change direction mid-way. Between pages, the browser's own View Transitions API needs no library (Page and view transitions, later in this track).
auto-animate respects a device set to reduce motion by default; Motion does inside its <MotionConfig reduced-motion="user"> wrapper.
What goes wrong
Two mistakes spoil the motion with no warning. In the first, the card vanishes instead of fading out.
<motion.article v-if="show" key="card" :exit="{ opacity: 0, y: -16 }">
Without <AnimatePresence>, Vue removes the card as soon as show changes, and :exit is never read.
<AnimatePresence>
<motion.article v-if="show" key="card" :exit="{ opacity: 0, y: -16 }">
</AnimatePresence>
Wrapped, the card fades out before it goes.
The second mistake keys each row by i, its position in the list, instead of its id.
<ul v-auto-animate>
<li v-for="(item, i) in saved" :key="i">{{ item.name }}</li>
</ul>
Remove Mug, the first of two rows. Vue keeps key 0 and changes its text to Teapot, then removes key 1, the bottom row. auto-animate animates exactly that: the bottom row fades out while the top row already reads Teapot, so for a moment the list shows Teapot twice.
<ul v-auto-animate>
<li v-for="item in saved" :key="item.id">{{ item.name }}</li>
</ul>
Keyed by item.id, Mug's own row fades out and Teapot glides up.
What to remember
- A
motion.divis a normal element with:initial,:animate,:exitand gesture props. You describe states; Motion animates between them. - A tween has a duration. A spring has stiffness and damping, overshoots, and keeps its velocity when interrupted.
:exitonly plays inside<AnimatePresence>, with akeyon the element. Without the wrapper the element is removed at once, and nothing warns.v-auto-animateon a parent animates its immediate children with no CSS. ImportvAutoAnimatein<script setup>and it is registered.- CSS first, auto-animate for lists that rearrange, Motion for springs and gestures, the View Transitions API between pages.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Replace the transition with
{ type: 'spring', stiffness: 300, damping: 18 }. Click Hide the card, then Show the card: the card overshoots its resting place and settles. Changedampingto 8: it bounces several times before it rests. Put 18 back. - Remove
v-auto-animatefrom the<ul>and click Save a bowl: the new row appears at once and the rows under it jump. Put it back asv-auto-animate="{ duration: 600 }": the new row fades in and the others glide down over most of a second, and Remove fades a row out while the gap closes. - Delete the
<AnimatePresence>and</AnimatePresence>lines and click Hide the card: the card disappears at once, with no warning. Put them back. Then change the list tov-for="(item, i) in saved" :key="i"and click Remove on Mug: the bottom row animates out while the top row already reads Teapot. Restore:key="item.id".