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
- •GPU-accelerated (best performance):
- •
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, // 100ms delay between children
delayChildren: 0.2 // Wait 200ms before starting
}
}
}
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>
)
}
// Advanced: Parallax scroll
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>
)
}
// Accordion example
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>
)
}
// Swipe to dismiss
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' }}
// or custom: ease: [0.4, 0, 0.2, 1]
- •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, // Higher = faster
damping: 30 // Higher = less bounce
}}
- •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
layoutprop)
Performance Optimization
GPU-Accelerated Properties
Animate These (60fps guaranteed):
// ✅ Fast (uses transform)
<motion.div
animate={{
x: 100, // translateX
y: 100, // translateY
scale: 1.5, // scale
rotate: 45, // rotate
opacity: 0.5 // opacity
}}
/>
Avoid Animating (causes reflow/repaint):
// ❌ Slow (triggers layout recalculation)
<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'
// Reduces bundle size by 30KB
export function OptimizedApp() {
return (
<LazyMotion features={domAnimation}>
<motion.div>Optimized animations</motion.div>
</LazyMotion>
)
}
Common Animation Patterns
Page Transitions (Next.js)
// app/layout.tsx
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:
- • Purpose: Does this animation serve a clear UX purpose?
- • Duration: Is it 200-500ms (or justified if longer)?
- • Easing: Using appropriate curve (ease-out for most cases)?
- • Performance: Animating only
transformandopacity? - • Reduced Motion: Respects
prefers-reduced-motion? - • Consistency: Matches timing/style of other animations?
- • Mobile: Tested on low-end devices?
- • Accessibility: Keyboard users can skip/disable?
- • Bundle Size: Using
LazyMotionif many animations?
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.