Skip to content
Back to Build Tutorials

Animation Handoff

Translate Figma Smart Animate / Lottie specs into Framer Motion or CSS animations in code.

Why

Animations are the most lossy part of the design-to-code handoff because they're specified as timing curves and keyframes in Figma but implemented as strings in code. A designer specifies '200ms ease-out with a 50ms stagger', and the developer writes 'transition: 0.2s' — missing the easing and stagger entirely. The disciplined approach is to define motion tokens (duration, easing, stagger) in the token system, reference them in both Figma and code, and use Framer Motion for complex sequences where CSS transitions can't express the choreography. Without this, animations feel janky and inconsistent because every developer picks different timing values.

When

When the design includes non-trivial motion (page transitions, staggered lists, spring physics). Takes 2-3 h. Skip for static designs.

How

1. Define motion tokens in the token system
   - duration.fast: 150ms, duration.base: 250ms, duration.slow: 400ms
   - easing.out: cubic-bezier(0, 0, 0.2, 1), easing.spring: spring(1, 100, 10, 0)

2. In Figma, use Smart Animate and document the timing in the frame name or comments
   - "Card hover — 200ms ease-out" as the frame name
   - Export Lottie for complex path animations (loading spinners, illustrations)

3. In code, use Framer Motion for orchestrated sequences, CSS for simple transitions
   - Framer Motion: layout animations, shared layout, drag, gestures, stagger
   - CSS: hover, focus, simple opacity/transform — no JS needed

4. Implement staggered lists with Framer Motion's staggerChildren
   - container variants: { hidden: {}, show: { transition: { staggerChildren: 0.05 } } }
   - item variants: { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 } }

5. Respect prefers-reduced-motion: disable all non-essential animation
   - Framer Motion: useReducedMotion() hook → swap to instant transitions
   - CSS: @media (prefers-reduced-motion: reduce) { * { transition: none !important } }

Example

// motion tokens (tokens.json)
{ "motion": { "duration": { "fast": "150ms", "base": "250ms" }, "easing": { "out": [0, 0, 0.2, 1] } } }

// Framer Motion staggered list
import { motion } from "framer-motion";
const container = { hidden: {}, show: { transition: { staggerChildren: 0.05 } } };
const item = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0, transition: { duration: 0.25, ease: [0, 0, 0.2, 1] } } };
export function List({ items }) {
  return (
    <motion.ul variants={container} initial="hidden" animate="show">
      {items.map(i => <motion.li key={i.id} variants={item}>{i.label}</motion.li>)}
    </motion.ul>
  );
}

// Reduced motion
import { useReducedMotion } from "framer-motion";
const reduce = useReducedMotion();
const transition = reduce ? { duration: 0 } : { duration: 0.25, ease: [0, 0, 0.2, 1] };

Common Problems

  • ⚠Using linear easing for everything, which looks robotic — natural motion decelerates (ease-out) or overshoots (spring).
  • ⚠Animations longer than 300ms for UI feedback, which feels sluggish — users perceive >100ms as a delay.
  • ⚠Not respecting prefers-reduced-motion, causing motion sickness for vestibular-sensitive users.
  • ⚠Animating layout properties (width, top) instead of transform, causing jank because layout triggers reflow.

Best Practices

  • ✓Define motion tokens and use them everywhere — never hardcode duration or easing in a component.
  • ✓Prefer transform and opacity for animations — they're GPU-accelerated and don't trigger reflow.
  • ✓Use Framer Motion's layout prop for shared element transitions (list → detail) — it handles the FLIP technique for you.
  • ✓Test with prefers-reduced-motion: reduce enabled to ensure the app is usable without animation.