Microinteractions and Animation Design
Timing curves, gesture feedback, and reduced-motion patterns for delightful yet accessible UI.
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
Example
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.