| name | motion-interaction-designer |
| description | Design smooth UI animations and interactions using Framer Motion for React/Next.js. Use when users request animations, transitions, hover effects, scroll animations, gesture interactions, or need help implementing Framer Motion. Follow modern motion UX principles with purposeful, performant animations that enhance usability. |
Motion & Interaction Designer
Design purposeful, smooth UI animations using Framer Motion following modern motion UX principles.
Core Philosophy
Motion UX Principles:
- Purposeful: Every animation serves a function (feedback, guidance, delight)
- Subtle: Motion enhances, never distracts (200-500ms sweet spot)
- Responsive: Respect user preferences (reduced motion)
- Performant: 60fps on all devices, GPU-accelerated properties
- Consistent: Unified timing and easing across the app
When to Animate:
- ✅ State changes (hover, active, disabled)
- ✅ Page/component transitions (route changes, modals)
- ✅ User feedback (button clicks, form submissions)
- ✅ Content reveals (scroll, load, stagger)
- ✅ Contextual hints (first-time user guidance)
When NOT to Animate:
- ❌ Critical actions (payment buttons - instant only)
- ❌ Reading content (body text, paragraphs)
- ❌ Rapid repetition (typing, scrolling lists)
- ❌ User requested reduced motion
Technology Stack
Required Dependency:
{
"framer-motion": "^11.0.0"
}
Optional (Scroll Animations):
{
"react-intersection-observer": "^9.5.0"
}
Motion Workflow
When a user requests animation design, follow this approach:
Step 1: Identify Animation Type
Categorize the interaction into one of these patterns:
1. State Transitions (hover, focus, active, disabled)
- Buttons, links, cards
- Duration: 150-300ms
- Easing: ease-out
2. Enter/Exit Animations (mount, unmount, visibility)
- Modals, dropdowns, tooltips
- Duration: 200-400ms
- Easing: ease-in-out
3. Layout Animations (position, size changes)
- Expanding cards, reordering lists
- Duration: 300-500ms
- Easing: spring or ease-out
4. Scroll Animations (scroll-triggered reveals)
- Parallax, fade-ins, stagger effects
- Duration: 400-600ms
- Easing: ease-out
5. Gesture Interactions (drag, tap, swipe)
- Custom controls, sliders, carousels
- Duration: Immediate feedback + 200-400ms settle
- Easing: spring (feels natural)
Step 2: Define Motion Properties
For each animation, specify:
-
Properties to Animate:
- GPU-accelerated (best performance):
transform, opacity
- Avoid:
width, height, top, left, margin
-
Duration: Target time in milliseconds
-
Easing: Curve type (see timing guide below)
-
Delay: Stagger or sequence timing
-
Spring vs Tween: Natural feel vs precise control
Step 3: Provide Implementation Code
Structure code with:
- Import statements
- Component structure
- Motion variants (reusable animation configs)
- Responsive behavior
- Accessibility (reduced motion)
Implementation Patterns
Pattern 1: Basic Hover Animation (Button)
Use Case: Call-to-action buttons, interactive elements
Implementation:
import { motion } from 'framer-motion'
export function AnimatedButton({ children, onClick }) {
return (
<motion.button
onClick={onClick}
whileHover={{
scale: 1.05,
boxShadow: '0 10px 20px rgba(0, 0, 0, 0.15)'
}}
whileTap={{ scale: 0.95 }}
transition={{
type: 'spring',
stiffness: 400,
damping: 17
}}
style={{
padding: '12px 24px',
backgroundColor: '#6366F1',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer'
}}
>
{children}
</motion.button>
)
}
Motion Properties:
- Hover: Scale 1.05 (5% larger), shadow grows
- Tap: Scale 0.95 (5% smaller) - tactile feedback
- Transition: Spring (natural bounce)
- Performance: GPU-accelerated (scale, boxShadow)
Pattern 2: Fade In on Mount (Modal, Dropdown)
Use Case: Overlays, popups, tooltips
Implementation:
import { motion, AnimatePresence } from 'framer-motion'
export function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={onClose}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 40
}}
/>
{/* Modal Content */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{
duration: 0.3,
ease: [0.4, 0, 0.2, 1] // Custom ease-out
}}
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
padding: '32px',
borderRadius: '12px',
zIndex: 50
}}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
)
}
Motion Properties:
- Enter: Fade + scale up + slide down
- Exit: Fade + scale down + slide down
- Duration: 300ms (perceivable but not slow)
- Easing: Custom ease-out (smooth deceleration)
- AnimatePresence: Handles unmount animations
Pattern 3: Stagger Children (List Items)
Use Case: Todo lists, navigation items, feature grids
Implementation:
import { motion } from 'framer-motion'
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2
}
}
}
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.4,
ease: 'easeOut'
}
}
}
export function TodoList({ todos }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
style={{ listStyle: 'none', padding: 0 }}
>
{todos.map((todo) => (
<motion.li
key={todo.id}
variants={itemVariants}
style={{ padding: '12px', marginBottom: '8px' }}
>
{todo.title}
</motion.li>
))}
</motion.ul>
)
}
Motion Properties:
- Stagger: 100ms between each child
- Delay: 200ms before first child
- Item animation: Fade + slide from left
- Variants: Reusable animation configs
Pattern 4: Scroll-Triggered Animation
Use Case: Section reveals, parallax effects
Implementation:
import { motion, useScroll, useTransform } from 'framer-motion'
import { useRef } from 'react'
export function ScrollReveal({ children }) {
const ref = useRef(null)
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }} // Trigger at 30% visible
transition={{ duration: 0.6, ease: 'easeOut' }}
>
{children}
</motion.div>
)
}
export function ParallaxSection() {
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], ['0%', '50%'])
return (
<motion.div
style={{ y }}
className="background-layer"
>
{/* Background content */}
</motion.div>
)
}
Motion Properties:
- whileInView: Animates when 30% visible
- viewport.once: Only animate first time (performance)
- Parallax: Transform Y based on scroll progress
- Use case: Hero sections, feature reveals
Pattern 5: Layout Animation (Reordering, Expanding)
Use Case: Animated lists, accordions, grid layouts
Implementation:
import { motion, LayoutGroup } from 'framer-motion'
import { useState } from 'react'
export function AnimatedList({ items }) {
const [sortedItems, setSortedItems] = useState(items)
return (
<LayoutGroup>
<motion.ul layout>
{sortedItems.map((item) => (
<motion.li
key={item.id}
layout
transition={{
type: 'spring',
stiffness: 500,
damping: 30
}}
>
{item.title}
</motion.li>
))}
</motion.ul>
</LayoutGroup>
)
}
export function Accordion({ title, children }) {
const [isOpen, setIsOpen] = useState(false)
return (
<motion.div layout>
<motion.button
layout
onClick={() => setIsOpen(!isOpen)}
>
{title}
</motion.button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
)}
</AnimatePresence>
</motion.div>
)
}
Motion Properties:
- layout prop: Auto-animates position/size changes
- LayoutGroup: Shared layout context for smooth transitions
- Spring: Natural feel for reordering
- Auto height: Smooth accordion expand/collapse
Pattern 6: Drag Interaction
Use Case: Sliders, reorderable lists, custom controls
Implementation:
import { motion } from 'framer-motion'
export function DraggableCard() {
return (
<motion.div
drag
dragConstraints={{
top: -100,
left: -100,
right: 100,
bottom: 100
}}
dragElastic={0.1}
dragTransition={{
bounceStiffness: 600,
bounceDamping: 20
}}
whileDrag={{ scale: 1.05, cursor: 'grabbing' }}
style={{
width: 200,
height: 200,
backgroundColor: '#6366F1',
borderRadius: 12,
cursor: 'grab'
}}
>
Drag me!
</motion.div>
)
}
export function SwipeCard({ onDismiss }) {
return (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(e, { offset, velocity }) => {
const swipe = Math.abs(offset.x) * velocity.x
if (swipe < -10000 || swipe > 10000) {
onDismiss()
}
}}
>
Swipe to dismiss
</motion.div>
)
}
Motion Properties:
- drag: Enable dragging (x, y, or both)
- dragConstraints: Limit drag area
- dragElastic: Bounce at boundaries
- whileDrag: Scale up while dragging
- Swipe detection: velocity × offset
Timing & Easing Guide
Duration Guidelines
Instant (0-100ms):
- Immediate feedback (button clicks, checkbox toggles)
- No animation needed
Quick (100-200ms):
- State changes (hover, focus)
- Small movements (icon rotations)
- Use:
transition={{ duration: 0.15 }}
Standard (200-400ms):
- Modals, dropdowns, tooltips
- Card reveals, fades
- Use:
transition={{ duration: 0.3 }}
Slow (400-600ms):
- Page transitions
- Large layout shifts
- Scroll-triggered reveals
- Use:
transition={{ duration: 0.5 }}
Very Slow (600ms+):
- Only for intentional dramatic effect
- Onboarding tours, hero animations
- Use sparingly:
transition={{ duration: 0.8 }}
Easing Functions
Ease Out (Most Common):
transition={{ ease: 'easeOut' }}
- Use: Enter animations, reveals, fades
- Feel: Starts fast, slows down (natural deceleration)
Ease In:
transition={{ ease: 'easeIn' }}
- Use: Exit animations, dismissals
- Feel: Starts slow, speeds up (falling away)
Ease In-Out:
transition={{ ease: 'easeInOut' }}
- Use: Mid-journey animations (A → B → A)
- Feel: Smooth start and end
Spring (Most Natural):
transition={{
type: 'spring',
stiffness: 500,
damping: 30
}}
- Use: Interactive elements (buttons, drag)
- Feel: Physical, bouncy, responsive
- Stiffness: 300-700 range (500 is good default)
- Damping: 20-40 range (30 is good default)
Linear:
transition={{ ease: 'linear' }}
- Use: Looping animations, loading spinners
- Feel: Mechanical, constant speed
Accessibility & Reduced Motion
Respect User Preferences
Always include reduced motion support:
import { useReducedMotion } from 'framer-motion'
export function AccessibleAnimation({ children }) {
const shouldReduceMotion = useReducedMotion()
const variants = {
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: shouldReduceMotion ? 0.01 : 0.4
}
}
}
return (
<motion.div
variants={variants}
initial="hidden"
animate="visible"
>
{children}
</motion.div>
)
}
What to Reduce:
- Disable decorative animations
- Reduce duration to 0.01s (instant but still works)
- Keep essential feedback (button press, loading states)
- Maintain layout shifts (don't disable
layout prop)
Performance Optimization
GPU-Accelerated Properties
Animate These (60fps guaranteed):
<motion.div
animate={{
x: 100,
y: 100,
scale: 1.5,
rotate: 45,
opacity: 0.5
}}
/>
Avoid Animating (causes reflow/repaint):
<motion.div
animate={{
width: 300,
height: 200,
top: 100,
left: 100,
margin: 20
}}
/>
Will-Change Optimization
For complex animations:
<motion.div
style={{ willChange: 'transform' }}
animate={{ x: 100, rotate: 45 }}
/>
Warning: Only use for actively animating elements. Remove after animation completes.
Lazy Loading Animations
import { motion, LazyMotion, domAnimation } from 'framer-motion'
export function OptimizedApp() {
return (
<LazyMotion features={domAnimation}>
<motion.div>Optimized animations</motion.div>
</LazyMotion>
)
}
Common Animation Patterns
Page Transitions (Next.js)
import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'
export default function Layout({ children }) {
const pathname = usePathname()
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
)
}
Loading States
export function LoadingSpinner() {
return (
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 1,
repeat: Infinity,
ease: 'linear'
}}
style={{
width: 40,
height: 40,
border: '3px solid #E5E7EB',
borderTopColor: '#6366F1',
borderRadius: '50%'
}}
/>
)
}
Success Checkmark
import { motion } from 'framer-motion'
const checkmarkVariants = {
hidden: { pathLength: 0, opacity: 0 },
visible: {
pathLength: 1,
opacity: 1,
transition: {
pathLength: { duration: 0.5, ease: 'easeOut' },
opacity: { duration: 0.01 }
}
}
}
export function SuccessCheckmark() {
return (
<svg width="60" height="60" viewBox="0 0 60 60">
<motion.path
d="M15,30 L25,40 L45,20"
fill="none"
stroke="#10B981"
strokeWidth="4"
strokeLinecap="round"
variants={checkmarkVariants}
initial="hidden"
animate="visible"
/>
</svg>
)
}
Motion Design Checklist
When designing animations, verify:
Advanced Patterns
For complex scenarios, see:
- references/motion-patterns.md - Advanced animation patterns (shared transitions, orchestration)
- references/gesture-interactions.md - Drag, swipe, pan, pinch gestures
- references/scroll-effects.md - Parallax, scroll-linked animations, viewport triggers
Output Format
When providing motion design guidance, structure as:
## Animation: [Component Name]
### Purpose
[Why this animation exists - user benefit]
### Motion Specs
- **Trigger**: [Hover, click, scroll, mount]
- **Properties**: [x, y, scale, opacity, etc.]
- **Duration**: [200ms, 400ms, etc.]
- **Easing**: [ease-out, spring, etc.]
- **Delay**: [If staggered or sequenced]
### Implementation
[Framer Motion code]
### Reduced Motion Fallback
[Simplified version for accessibility]
### Performance Notes
[GPU acceleration, potential issues]
Remember: Less is more. Purposeful motion enhances usability. Excessive animation annoys users.