Skip to content
Back to Design Hub

Microinteractions and Animation Design

Timing curves, gesture feedback, and reduced-motion patterns for delightful yet accessible UI.

Component PatternsAdvanced

Why

Microinteractions (button ripples, toggle springs, loading shimmers) communicate state changes 10x faster than text. But poorly tuned animations cause vestibular distress and feel janky.

A disciplined motion system balances delight with performance and accessibility.

When

Add motion for state transitions (loading, success, error), gesture feedback (swipe, drag), and focus guidance. Avoid animating decorative elements that compete with content for attention.

How

1. Define motion tokens — duration (150/250/400ms), easing (ease-out, spring), stagger 2. Choose library — Framer Motion for React, Motion One for vanilla, GSAP for complex timelines 3. Use springs over linear — natural feel, e.g. transition: { type: "spring", stiffness: 300, damping: 30 } 4. Animate transform + opacity only — avoid animating layout properties (width, top) for 60fps 5. Respect reduced motion — wrap in useReducedMotion() hook, fall back to instant state changes 6. Orchestrate — stagger children with delay: i * 0.05, exit animations with AnimatePresence 7. Profile — React DevTools Profiler + Chrome Performance tab, target <16ms per frame

Example

// motion-tokens.ts — shared timing vocabulary export const motion = { fast: { duration: 0.15, ease: [0.4, 0, 1, 1] as const }, normal: { duration: 0.25, ease: [0.4, 0, 0.2, 1] as const }, spring: { type: "spring", stiffness: 300, damping: 30, mass: 1 }, stagger: { staggerChildren: 0.05 }, } as const; // Toast.tsx — spring entrance + reduced-motion guard import { motion, useReducedMotion } from "framer-motion"; function Toast({ message }: { message: string }) { const reduce = useReducedMotion(); return ( <motion.div initial={reduce ? { opacity: 0 } : { opacity: 0, y: 20, scale: 0.95 }} animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} transition={reduce ? motion.fast : motion.spring} className="rounded-lg bg-slate-900 px-4 py-2 text-white" > {message} </motion.div> ); } // List stagger — children cascade in <motion.ul variants={{ hidden: {}, show: motion.stagger }} initial="hidden" animate="show"> {items.map((item) => ( <motion.li variants={{ hidden: { opacity: 0, y: 10 }, show: { opacity: 1, y: 0, transition: motion.normal } }}>{item}</motion.li> ))} </motion.ul>

Common Problems

  • ⚠Animating width/height/top — triggers layout recalculation every frame, drops to 20fps
  • ⚠No reduced-motion fallback — vestibular users get nauseous from parallax and springs
  • ⚠Linear easing everywhere — feels robotic, no acceleration/deceleration curve
  • ⚠Animations on mount of every list item — 100 items stagger = 5s before last appears

Best Practices

  • ✓Animate only transform and opacity — GPU-composited, no layout thrash
  • ✓Cap stagger at 5-8 items — beyond that, fade the container instead of cascading
  • ✓Always provide useReducedMotion branch — instant state change, no motion
  • ✓Define motion tokens once — every animation references motion.normal, not magic numbers

FAQ

Why animate transform/opacity and not width/top?

Transform and opacity are GPU-composited and don't trigger layout recalculation. Animating width, top, or left recalculates layout every frame, dropping to 20fps.

How do I respect users with reduced-motion preference?

Use the useReducedMotion() hook (Framer Motion) or matchMedia('(prefers-reduced-motion: reduce)'). When set, skip animations and apply the final state instantly.

Linear vs spring easing — which feels better?

Springs (stiffness/damping) feel natural with acceleration and settle; linear feels robotic. Use springs for interactive elements, ease-out for entrances, never linear for UI.

How do I stagger a long list without waiting 5 seconds?

Cap stagger at 5-8 items. Beyond that, fade the whole container in instead of cascading each item — 100 items × 50ms = 5s before the last appears.

Where should I define my animation timing?

In motion tokens (duration 150/250/400ms, easing curves) shared across the app. Every animation references motion.normal, never magic numbers — keeps timing consistent.