motion-vue
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
-
Use motion values instead of Vue state for frequently-updated styles:
<script setup> const x = useMotionValue(0) // No re-renders on change </script> -
Add
willChangefor transform-heavy animations:<motion.div :style="{ willChange: 'transform' }" /> -
Use
layoutsparingly—it triggers measurements -
Prefer independent transforms (
x,y,scale,rotate) overtransformstring -
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 |
More from akornmeier/claude-config
docker
Guide for using Docker - a containerization platform for building, running, and deploying applications in isolated containers. Use when containerizing applications, creating Dockerfiles, working with Docker Compose, managing images/containers, configuring networking and storage, optimizing builds, deploying to production, or implementing CI/CD pipelines with Docker.
12postgresql-psql
Comprehensive guide for PostgreSQL psql - the interactive terminal client for PostgreSQL. Use when connecting to PostgreSQL databases, executing queries, managing databases/tables, configuring connection options, formatting output, writing scripts, managing transactions, and using advanced psql features for database administration and development.
9prd-to-ux
Use when translating PRDs, feature specs, or product requirements into UX specifications for mockup tools like Google Stitch, Figma, or similar. Use before creating visual designs, wireframes, or component specs.
6chrome-debug
How to use Chrome DevTools MCP for browser debugging. Use when you need to inspect pages, take screenshots, debug UI issues, or verify visual changes.
6tailwindcss
Guide for implementing Tailwind CSS - a utility-first CSS framework for rapid UI development. Use when styling applications with responsive design, dark mode, custom themes, or building design systems with Tailwind's utility classes.
5nuxt-ui
Expert guide for implementing NuxtUI v4.1+ - an open-source UI library of 100+ customizable components built with Tailwind CSS and Reka UI. Use when the user mentions NuxtUI components (UButton, UInput, UCard, etc.), asks about implementing UI features, needs component examples, works with forms/layouts/navigation, or requests theme/styling customization. Proactively suggest when building UI to ensure consistent design. (project)
4