---
title: "Motion for Vue 2.0: what changed · vuejs.school"
description: "motion-v 2.0 is ESM only, drops the old hover and press shorthand props, and adds a v-motion directive for animating any element without a wrapper."
canonical_url: "https://vuejs.school/community/blog/motion-for-vue-2-what-changed"
meta:
  "og:description": "motion-v 2.0 is ESM only, drops the old hover and press shorthand props, and adds a v-motion directive for animating any element without a wrapper."
  "og:image": "https://vuejs.school/_og/d/a_~TW90aW9uIGZvciBWdWUgMi4wOiB3aGF0IGNoYW5nZWQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TW90aW9uIGZvciBWdWUgMi4wOiB3aGF0IGNoYW5nZWQ%2Cdescription_~bW90aW9uLXYgMi4wIGlzIEVTTSBvbmx5LCBkcm9wcyB0aGUgb2xkIGhvdmVyIGFuZCBwcmVzcyBzaG9ydGhhbmQgcHJvcHMsIGFuZCBhZGRzIGEgdi1tb3Rpb24gZGlyZWN0aXZlIGZvciBhbmltYXRpbmcgYW55IGVsZW1lbnQgd2l0aG91dCBhIHdyYXBwZXIu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9tb3Rpb24tZm9yLXZ1ZS0yLXdoYXQtY2hhbmdlZCI%2Cs_XZaR_3HAqzkhY4SF.png"
  "og:image:alt": "Motion for Vue 2.0: what changed · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Motion for Vue 2.0: what changed · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~TW90aW9uIGZvciBWdWUgMi4wOiB3aGF0IGNoYW5nZWQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TW90aW9uIGZvciBWdWUgMi4wOiB3aGF0IGNoYW5nZWQ%2Cdescription_~bW90aW9uLXYgMi4wIGlzIEVTTSBvbmx5LCBkcm9wcyB0aGUgb2xkIGhvdmVyIGFuZCBwcmVzcyBzaG9ydGhhbmQgcHJvcHMsIGFuZCBhZGRzIGEgdi1tb3Rpb24gZGlyZWN0aXZlIGZvciBhbmltYXRpbmcgYW55IGVsZW1lbnQgd2l0aG91dCBhIHdyYXBwZXIu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9tb3Rpb24tZm9yLXZ1ZS0yLXdoYXQtY2hhbmdlZCI%2Cs_XZaR_3HAqzkhY4SF.png"
  "twitter:image:alt": "Motion for Vue 2.0: what changed · vuejs.school"
---

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

## What shipped

The release notes and the beta notes list these changes:

- **Breaking:** the package is ESM only. The CommonJS build is gone.
- **Breaking:** the deprecated gesture shorthand props `hover`, `press`, `focus` and `inView` were removed.
- **New:** a `v-motion` directive animates any HTML or SVG element without a `<motion>` wrapper. It accepts the animation, gesture, layout and exit props, and `MotionPlugin` can register your own preset directives.
- **Internal:** imports moved from `framer-motion` internals to the public `motion-dom` and `motion-utils` packages, and the feature loading was rebuilt on the same feature-bundle design as Motion for React, for better tree-shaking. Your code needs no change for this unless you use `LazyMotion` (see the next section).

The betas also fixed several bugs around exits and layout:

- a scheduling deadlock around `onExitComplete`
- a reorder list that froze when dragged during an exit
- a stray layout animation on mount inside an animating parent
- Vue's CSS transition handling, now switched off inside `AnimatePresence`

The documentation folder left the repository; the docs live at [motion.dev/docs/vue](https://motion.dev/docs/vue).

## Two breaking changes: ESM only and the old gesture props

ESM only means `require('motion-v')` no longer works. Vite, Nuxt and Vitest load packages as ES modules already, so most Vue projects never notice. The places that do are older Jest setups and Node scripts written with `require`.

The `exports` field of the package's `package.json`, which tells Node which files the package offers, now lists only `import` entries, for the main entry, `motion-v/nuxt` and `motion-v/resolver`. With no `require` entry, Node stops with `ERR_PACKAGE_PATH_NOT_EXPORTED`, even in versions that can load ES modules through `require`.

The removed props were aliases kept from early versions. In 1.x the types marked `hover` as deprecated with the note "Use `whileHover` instead", and the same applied to the other three. In 2.0 they are no longer props at all, so code that still uses them stops animating without an error:

*src/components/LikeButton.vue**Breaks***

```
<script setup lang="ts">
import { motion } from 'motion-v'
</script>

<template>
  <motion.button type="button" :hover="{ scale: 1.1 }" :press="{ scale: 0.95 }">
    Like
  </motion.button>
</template>
```

Rename each one to its `while` form: `hover` to `whileHover`, `press` to `whilePress`, `focus` to `whileFocus`, `inView` to `whileInView`.

*src/components/LikeButton.vue**Works***

```
<script setup lang="ts">
import { motion } from 'motion-v'
</script>

<template>
  <motion.button type="button" :whileHover="{ scale: 1.1 }" :whilePress="{ scale: 0.95 }">
    Like
  </motion.button>
</template>
```

One smaller change sits in the types rather than the notes, and it only matters if you use `LazyMotion`. Its `features` prop is now required and takes a feature bundle, `domAnimation` or `domMax`, or a function that loads one. Code that passes those two exports keeps working.

## Animate any element with `v-motion`

A directive is an attribute that starts with `v-` and runs code against the element it sits on. Until now, animating an element meant replacing it with a motion component: `<h1>` became `<motion.h1>`. The directive leaves your markup alone. Import `vMotion` in `<script setup>` and it becomes available as `v-motion`:

*src/components/PageTitle.vue*

```
<script setup lang="ts">
import { vMotion } from 'motion-v'
</script>

<template>
  <h1
    v-motion
    :initial="{ opacity: 0, y: 12 }"
    :animate="{ opacity: 1, y: 0 }"
    :transition="{ duration: 0.4 }"
  >
    Your cards
  </h1>
</template>
```

The options can sit on the element as separate props, as above, or go into the directive's value as one object: `v-motion="{ initial: { opacity: 0 }, animate: { opacity: 1 } }"`. When both are present, the element's props win.

The directive reads context from parent components too. An element with `v-motion` and `exit` inside `AnimatePresence` animates out when `v-if` removes it.

To register the directive globally, install `MotionPlugin`. Its `presets` option turns a set of options into a directive of its own:

*src/main.ts*

```
import { createApp } from 'vue'
import { MotionPlugin } from 'motion-v'
import App from './App.vue'

createApp(App)
  .use(MotionPlugin, {
    presets: {
      'fade-in': { initial: { opacity: 0 }, animate: { opacity: 1 } },
    },
  })
  .mount('#app')
```

Any template can now use `v-motion` and `<p v-fade-in>`. In Nuxt, the module does the same from `nuxt.config.ts`: `directives: true` registers `v-motion` (it is off by default), and `presets` adds your own:

*nuxt.config.ts*

```
export default defineNuxtConfig({
  modules: ['motion-v/nuxt'],
  motionV: {
    directives: true,
    presets: {
      'fade-in': { initial: { opacity: 0 }, animate: { opacity: 1 } },
    },
  },
})
```

## A card list with enter, exit and layout animations

Apart from the removed aliases and `LazyMotion`, the component API is unchanged, so this list runs on 1.10 as well as 2.0. Cards fade in from above, shrink away when removed, and slide into place when the list is sorted:

*src/components/CardList.vue*

```
<script setup lang="ts">
import { ref } from 'vue'
import { AnimatePresence, motion } from 'motion-v'

interface Card {
  id: number
  title: string
}

const cards = ref<Card[]>([
  { id: 1, title: 'Slots' },
  { id: 2, title: 'Props' },
  { id: 3, title: 'Events' },
])
let nextId = 4

function add() {
  cards.value.unshift({ id: nextId, title: `Card ${nextId}` })
  nextId++
}

function remove(id: number) {
  cards.value = cards.value.filter(card => card.id !== id)
}

function sortByTitle() {
  cards.value = [...cards.value].sort((a, b) => a.title.localeCompare(b.title))
}
</script>

<template>
  <button type="button" @click="add">Add card</button>
  <button type="button" @click="sortByTitle">Sort A to Z</button>
  <ul class="cards">
    <AnimatePresence mode="popLayout">
      <motion.li
        v-for="card in cards"
        :key="card.id"
        layout
        :initial="{ opacity: 0, y: -16 }"
        :animate="{ opacity: 1, y: 0 }"
        :exit="{ opacity: 0, scale: 0.9 }"
        :whileHover="{ scale: 1.02 }"
      >
        {{ card.title }}
        <button type="button" @click="remove(card.id)">Remove</button>
      </motion.li>
    </AnimatePresence>
  </ul>
</template>

<style scoped>
.cards {
  position: relative;
  list-style: none;
  padding: 0;
}
</style>
```

What each part does:

- `initial` is where a new card starts and `animate` is where it ends up.
- `exit` only works because `AnimatePresence` wraps the list: it keeps a removed card in the page until its exit animation finishes.
- The `:key` must be the card's id, never the index, or Motion cannot tell which card left.
- `layout` animates cards that change position. When a card is added at the top or the list is sorted, each card measures where it was and where it is now, then animates the difference with transforms.
- `mode="popLayout"` takes a leaving card out of the flow at once, so the cards below slide up while it fades instead of waiting. The docs ask for a positioned parent in that mode, which is why `.cards` has `position: relative`.

Cards present on first render animate in too. Pass `:initial="false"` to `AnimatePresence` if they should simply appear.

## Choose between Motion and Vue's Transition

Vue ships `<Transition>` and `<TransitionGroup>`. They add CSS classes such as `v-enter-from` and `v-leave-to` at the right moments, and you write the animation in CSS. `<TransitionGroup>` also moves list items with a `v-move` class. That covers fades, slides and simple list changes, and it ships with Vue, so there is nothing to install.

Motion runs the animation in JavaScript instead. Springs carry on from the velocity of an interrupted animation, hover gestures ignore touch-screen taps, and layout animations run on transforms with scale correction, between the same or different elements.

`AnimatePresence` is itself built on Vue's components: its source renders a `TransitionGroup`, or a `Transition` in `wait` mode, with CSS turned off, and Motion runs in the enter and leave hooks.

Use `<Transition>` for a toggle that fades or slides. Use Motion when the animation reacts to the user, when the layout changes size or order, or when an element should move from one place to another with `layoutId`.

## How to upgrade

Install the new major:

```
npm install motion-v@2
```

Then work through three checks:

1. Search your components for `hover=`, `press=`, `focus=` and `inView=` on motion components and rename each to its `while` form. Leave the colon out of the search, so it also finds a variant name passed as a plain string, such as `hover="grow"`.
2. If tests run in Jest with CommonJS, move them to Vitest. Jest would need extra configuration to resolve and transform the ES build, because there is no `require` entry to fall back on.
3. In Nuxt, keep `'motion-v/nuxt'` in `modules`. The component and composable auto-imports work as before, and `vMotion` and `createPresetDirective` join the list.

Upgrade now. The breaking changes are small and easy to find with a search, the betas fixed real exit and layout bugs, and `v-motion` lets you animate a heading or a list item without restructuring the template. The only reason to wait is a CommonJS test setup you cannot change this week.

**Check it stuck**3 questions from the Motion for Vue and auto-animate quiz.

[**Practice now**](https://vuejs.school/learn/animation/motion-libraries-and-view-transitions?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) [Tutorial · 6 min read**Animate lists with \<TransitionGroup>**](https://vuejs.school/community/blog/animate-lists-with-transitiongroup)

```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/motion-for-vue-2-what-changed#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-02-26","description":"motion-v 2.0 is ESM only, drops the old hover and press shorthand props, and adds a v-motion directive for animating any element without a wrapper.","name":"Motion for Vue 2.0: what changed","url":"https://vuejs.school/community/blog/motion-for-vue-2-what-changed","breadcrumb":{"@id":"https://vuejs.school/community/blog/motion-for-vue-2-what-changed#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/motion-for-vue-2-what-changed"]}],"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/motion-for-vue-2-what-changed#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-02-26","description":"motion-v 2.0 is ESM only, drops the old hover and press shorthand props, and adds a v-motion directive for animating any element without a wrapper.","headline":"Motion for Vue 2.0: what changed","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~TW90aW9uIGZvciBWdWUgMi4wOiB3aGF0IGNoYW5nZWQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TW90aW9uIGZvciBWdWUgMi4wOiB3aGF0IGNoYW5nZWQ%2Cdescription_~bW90aW9uLXYgMi4wIGlzIEVTTSBvbmx5LCBkcm9wcyB0aGUgb2xkIGhvdmVyIGFuZCBwcmVzcyBzaG9ydGhhbmQgcHJvcHMsIGFuZCBhZGRzIGEgdi1tb3Rpb24gZGlyZWN0aXZlIGZvciBhbmltYXRpbmcgYW55IGVsZW1lbnQgd2l0aG91dCBhIHdyYXBwZXIu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9tb3Rpb24tZm9yLXZ1ZS0yLXdoYXQtY2hhbmdlZCI%2Cs_XZaR_3HAqzkhY4SF.png","@type":["Article","BlogPosting"],"articleSection":["Release notes"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/motion-for-vue-2-what-changed#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/motion-for-vue-2-what-changed#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/motion-for-vue-2-what-changed#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Motion for Vue 2.0: what changed","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_~TW90aW9uIGZvciBWdWUgMi4wOiB3aGF0IGNoYW5nZWQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TW90aW9uIGZvciBWdWUgMi4wOiB3aGF0IGNoYW5nZWQ%2Cdescription_~bW90aW9uLXYgMi4wIGlzIEVTTSBvbmx5LCBkcm9wcyB0aGUgb2xkIGhvdmVyIGFuZCBwcmVzcyBzaG9ydGhhbmQgcHJvcHMsIGFuZCBhZGRzIGEgdi1tb3Rpb24gZGlyZWN0aXZlIGZvciBhbmltYXRpbmcgYW55IGVsZW1lbnQgd2l0aG91dCBhIHdyYXBwZXIu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9tb3Rpb24tZm9yLXZ1ZS0yLXdoYXQtY2hhbmdlZCI%2Cs_XZaR_3HAqzkhY4SF.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~TW90aW9uIGZvciBWdWUgMi4wOiB3aGF0IGNoYW5nZWQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TW90aW9uIGZvciBWdWUgMi4wOiB3aGF0IGNoYW5nZWQ%2Cdescription_~bW90aW9uLXYgMi4wIGlzIEVTTSBvbmx5LCBkcm9wcyB0aGUgb2xkIGhvdmVyIGFuZCBwcmVzcyBzaG9ydGhhbmQgcHJvcHMsIGFuZCBhZGRzIGEgdi1tb3Rpb24gZGlyZWN0aXZlIGZvciBhbmltYXRpbmcgYW55IGVsZW1lbnQgd2l0aG91dCBhIHdyYXBwZXIu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9tb3Rpb24tZm9yLXZ1ZS0yLXdoYXQtY2hhbmdlZCI%2Cs_XZaR_3HAqzkhY4SF.png"}]}
```