Back to skills
extension
Category: Development & EngineeringNo API key required

motion-vue

Motion for Vue animation library guidance. Use when building Vue animations with motion-v package, implementing gesture animations (hover, press, drag), scroll-linked animations, layout animations, exit animations with AnimatePresence, variants, spring physics, or using hooks like useAnimate, useScroll, useSpring, useMotionValue, useTransform. Triggers on keywords like motion.div, motion-v, whileHover, whilePress, whileDrag, whileInView, AnimatePresence, layout animations, scroll animations, MotionConfig, LayoutGroup.

personAuthor: jakexiaohubgithub

Motion for Vue

Motion for Vue (motion-v) is a production-ready animation library with a hybrid engine capable of hardware-accelerated 120fps animations. This skill provides patterns and best practices for building performant, accessible Vue animations.

Installation

npm install motion-v

Nuxt Integration

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['motion-v/nuxt'],
})

unplugin-vue-components

import Components from 'unplugin-vue-components/vite'
import MotionResolver from 'motion-v/resolver'

export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [MotionResolver()],
    }),
  ],
})

Note: Auto-import doesn't support <motion /> component—import manually.

Core Concepts

The motion Component

Every HTML/SVG element has a motion equivalent: motion.div, motion.button, motion.circle, etc.

<script setup>
import { motion } from 'motion-v'
</script>

<template>
  <motion.div
    :initial="{ opacity: 0, y: 20 }"
    :animate="{ opacity: 1, y: 0 }"
    :transition="{ duration: 0.5 }"
  />
</template>

Animation Props

| Prop | Purpose | |------|---------| | initial | Starting state (or false to skip enter animation) | | animate | Target state to animate to | | exit | State when removed (requires AnimatePresence) | | transition | Animation configuration | | variants | Named animation states | | whileHover | State during hover | | whilePress | State during press/tap | | whileDrag | State during drag | | whileInView | State when in viewport | | whileFocus | State when focused | | layout | Enable layout animations | | layoutId | Shared element transitions |

Animation Patterns

Basic Animation

<motion.div
  :initial="{ opacity: 0, scale: 0.8 }"
  :animate="{ opacity: 1, scale: 1 }"
  :transition="{ type: 'spring', stiffness: 300, damping: 20 }"
/>

Gesture Animations

<motion.button
  :whileHover="{ scale: 1.05, backgroundColor: '#3b82f6' }"
  :whilePress="{ scale: 0.95 }"
  :transition="{ type: 'spring', stiffness: 400, damping: 17 }"
  @hoverStart="() => console.log('hover started')"
  @hoverEnd="() => console.log('hover ended')"
  @pressStart="(e) => console.log('press started', e)"
  @press="(e) => console.log('press complete', e)"
/>

Drag Gestures

<script setup>
import { motion, useDomRef } from 'motion-v'

const constraintsRef = useDomRef()
</script>

<template>
  <motion.div ref="constraintsRef" class="container">
    <motion.div
      drag
      :dragConstraints="constraintsRef"
      :dragElastic="0.2"
      :dragMomentum="true"
      :whileDrag="{ scale: 1.1, cursor: 'grabbing' }"
    />
  </motion.div>
</template>

Variants (Orchestration)

<script setup>
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      when: 'beforeChildren',
      staggerChildren: 0.1
    }
  }
}

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 }
}
</script>

<template>
  <motion.ul
    :variants="containerVariants"
    initial="hidden"
    animate="visible"
  >
    <motion.li
      v-for="item in items"
      :key="item.id"
      :variants="itemVariants"
    >
      {{ item.name }}
    </motion.li>
  </motion.ul>
</template>

Exit Animations (AnimatePresence)

<script setup>
import { motion, AnimatePresence } from 'motion-v'
import { ref } from 'vue'

const isVisible = ref(true)
</script>

<template>
  <AnimatePresence>
    <motion.div
      v-if="isVisible"
      key="modal"
      :initial="{ opacity: 0, scale: 0.9 }"
      :animate="{ opacity: 1, scale: 1 }"
      :exit="{ opacity: 0, scale: 0.9 }"
    />
  </AnimatePresence>
</template>

Critical: Direct children of AnimatePresence must have unique key props.

AnimatePresence Modes

<!-- sync (default): Enter/exit simultaneously -->
<AnimatePresence mode="sync">

<!-- wait: New child waits for exiting child -->
<AnimatePresence mode="wait">

<!-- popLayout: Exiting children pop out of layout flow -->
<AnimatePresence mode="popLayout">

Dynamic Exit with custom Prop

<script setup>
const variants = {
  enter: (direction) => ({
    x: direction > 0 ? 300 : -300,
    opacity: 0
  }),
  center: { x: 0, opacity: 1 },
  exit: (direction) => ({
    x: direction < 0 ? 300 : -300,
    opacity: 0
  })
}
</script>

<template>
  <AnimatePresence :custom="direction">
    <motion.div
      :key="currentPage"
      :custom="direction"
      :variants="variants"
      initial="enter"
      animate="center"
      exit="exit"
    />
  </AnimatePresence>
</template>

Layout Animations

<!-- Animate layout changes -->
<motion.div layout :style="{ width: isExpanded ? '200px' : '100px' }" />

<!-- Shared element transitions -->
<motion.div v-if="isSelected" layoutId="highlight" />

Important: CSS changes should happen via :style, not :animate—layout handles the animation.

LayoutGroup for Coordination

<script setup>
import { motion, LayoutGroup, AnimatePresence } from 'motion-v'
</script>

<template>
  <LayoutGroup>
    <motion.ul layout>
      <AnimatePresence>
        <motion.li
          v-for="item in items"
          :key="item.id"
          layout
          :exit="{ opacity: 0, scale: 0.8 }"
        />
      </AnimatePresence>
    </motion.ul>
  </LayoutGroup>
</template>

Scroll Animations

Scroll-Triggered (whileInView)

<motion.div
  :initial="{ opacity: 0, y: 50 }"
  :whileInView="{ opacity: 1, y: 0 }"
  :inViewOptions="{ once: true, margin: '-100px' }"
/>

Scroll-Linked (useScroll)

<script setup>
import { motion, useScroll, useSpring, useTransform } from 'motion-v'

const { scrollYProgress } = useScroll()
const scaleX = useSpring(scrollYProgress, {
  stiffness: 100,
  damping: 30,
  restDelta: 0.001
})
</script>

<template>
  <motion.div class="progress-bar" :style="{ scaleX }" />
</template>

Element Progress Tracking

<script setup>
import { ref } from 'vue'
import { motion, useScroll, useTransform } from 'motion-v'

const targetRef = ref(null)
const { scrollYProgress } = useScroll({
  target: targetRef,
  offset: ['start end', 'end start']  // When element enters/leaves viewport
})

const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0])
const y = useTransform(scrollYProgress, [0, 1], [100, -100])
</script>

<template>
  <motion.div ref="targetRef" :style="{ opacity, y }" />
</template>

Motion Values & Hooks

See HOOKS_REFERENCE.md for complete API documentation.

Quick Reference

| Hook | Purpose | |------|---------| | useMotionValue(initial) | Reactive animated value (no re-renders) | | useSpring(source, config) | Spring-based motion value | | useTransform(value, input, output) | Map values to new values | | useVelocity(value) | Track velocity of motion value | | useScroll(options) | Track scroll position/progress | | useAnimate() | Imperative animation control | | useInView(ref, options) | Viewport intersection detection | | useReducedMotion() | User motion preference | | useAnimationFrame(callback) | Per-frame callbacks | | useTime() | Elapsed time as motion value | | useDragControls() | Programmatic drag control | | useMotionTemplate | Template string with motion values | | useDomRef() | DOM ref for constraints |

Transition Options

<!-- Tween (default) -->
<motion.div :animate="{ x: 100 }" :transition="{ duration: 0.5, ease: 'easeInOut' }" />

<!-- Spring (physics-based) -->
<motion.div :animate="{ scale: 1.2 }" :transition="{ type: 'spring', stiffness: 300, damping: 20 }" />

<!-- Spring (duration-based) -->
<motion.div :animate="{ rotate: 180 }" :transition="{ type: 'spring', duration: 0.8, bounce: 0.25 }" />

<!-- Per-value transitions -->
<motion.div
  :animate="{ x: 100, opacity: 1 }"
  :transition="{ default: { type: 'spring' }, opacity: { ease: 'linear', duration: 0.2 } }"
/>

<!-- Keyframes with timing -->
<motion.div
  :animate="{ x: [0, 100, 50, 100], transition: { duration: 2, times: [0, 0.3, 0.6, 1] } }"
/>

<!-- Repeat/loop -->
<motion.div :animate="{ rotate: 360 }" :transition="{ repeat: Infinity, repeatType: 'loop', duration: 2 }" />

Spring options: stiffness, damping, mass, bounce, duration, restDelta, restSpeed Tween options: duration, ease, delay Repeat options: repeat (count or Infinity), repeatType ('loop'|'reverse'|'mirror'), repeatDelay

Global Configuration

MotionConfig

<script setup>
import { motion, MotionConfig } from 'motion-v'
</script>

<template>
  <MotionConfig
    :transition="{ duration: 0.3, ease: 'easeOut' }"
    reducedMotion="user"
  >
    <App />
  </MotionConfig>
</template>

reducedMotion options:

  • "user" (default): Respect device settings
  • "always": Force reduced motion
  • "never": Ignore preference

Custom Components

Wrap any Vue component with motion capabilities:

<script setup>
import { motion } from 'motion-v'
import MyButton from './MyButton.vue'

// IMPORTANT: Define outside template to prevent recreation each render
const MotionButton = motion.create(() => MyButton)
</script>

<template>
  <MotionButton
    :whileHover="{ scale: 1.05 }"
    :whilePress="{ scale: 0.95 }"
  />
</template>

Performance Best Practices

  1. Use motion values instead of Vue state for frequently-updated styles:

    <script setup>
    const x = useMotionValue(0)  // No re-renders on change
    </script>
    
  2. Add willChange for transform-heavy animations:

    <motion.div :style="{ willChange: 'transform' }" />
    
  3. Use layout sparingly—it triggers measurements

  4. Prefer independent transforms (x, y, scale, rotate) over transform string

  5. Use initial={false} to skip enter animations when not needed

Accessibility

Always respect user preferences:

<script setup>
import { useReducedMotion } from 'motion-v'

const prefersReducedMotion = useReducedMotion()
</script>

<template>
  <motion.div
    :animate="prefersReducedMotion ? {} : { x: 100 }"
    :transition="prefersReducedMotion ? { duration: 0 } : { duration: 0.5 }"
  />
</template>

Common Patterns

See PATTERNS.md for complete examples including:

  • Modal dialogs with backdrop
  • Accordion/collapsible content
  • Tab indicators with shared layout
  • Staggered list reveals
  • Page transitions
  • Draggable reorder lists
  • Scroll progress indicators
  • Parallax effects

Troubleshooting

| Issue | Solution | |-------|----------| | Exit animations not working | Ensure direct child of AnimatePresence has key prop | | Layout animation jittery | Add layoutScroll to scrollable ancestors | | Animation not smooth | Check for re-renders; use motion values | | Gesture not working on SVG filter | Add gesture props to parent, use variants | | Touch hover issues | Motion handles this automatically; don't use CSS :hover |