Animation Basics

Understanding Framer Motion Timeline Basics

When to Use Timeline

Initially, I used different delays to create sequence animations, but that's not the optimal approach. Instead, we should use timeline for better control: first move, then scale, then whatever you want - it's simple and easy. Use Framer Motion's timeline when you need animations to happen one after another in a specific order. Instead of trying to manage multiple animation hooks or complex timing with useEffect, timeline lets you control exactly when each animation starts and ends. This is useful for things like loading screens, button interactions, or any animation that has multiple steps.

useAnimate vs useAnimation

The key difference is about scope and control:
useAnimate: Controls multiple elements at once using CSS selectors
  • You can animate #element1, #element2, .class-name all in one timeline
  • Perfect for orchestrating complex animations across multiple elements
  • The scope defines which elements you can target
  • Can be triggered by button clicks, scroll, or any event
useAnimation: Controls one specific element
  • You attach it directly to a single motion.div or motion.span
  • More direct control over that one element
  • Better for simple, single-element animations
  • Can also be triggered by button clicks or any event
Simple rule: One element → use useAnimation | Multiple elements → use useAnimate

Multiple Elements with useAnimate

The useAnimate hook is perfect for controlling multiple elements in sequence. Here's an example that shows how to animate different elements one after another using CSS selectors:
import { motion, useAnimate } from "framer-motion";
import { useState } from "react";

const MultipleElementsExample = () => {
  const [scope, animate] = useAnimate();
  const [isAnimating, setIsAnimating] = useState(false);

  const SPRING = {
    type: "spring" as const,
    stiffness: 80,
    damping: 10,
  };

  const handleClick = async () => {
    if (isAnimating) return;
    
    setIsAnimating(true);
    
    const runAnimation = async () => {
      // Step 1: Animate the first circle
      await animate("#circle-1", { y: 0, scale: 1.2 }, SPRING);

      // Step 2: Animate the second circle
      await animate("#circle-2", { y: 0, scale: 1.2 }, SPRING);

      // Step 3: Animate the third circle
      await animate("#circle-3", { y: 0, scale: 1.2 }, SPRING);

      // Step 4: Reset all circles
      await animate("#circle-1, #circle-2, #circle-3", { scale: 1 }, SPRING);
    };

    await runAnimation();
    setIsAnimating(false);
  };

  return (
    <div className="flex flex-col items-center gap-4">
      <button
        onClick={handleClick}
        disabled={isAnimating}
        className="rounded-lg bg-blue-500 px-4 py-2 text-white disabled:opacity-50"
      >
        {isAnimating ? "Animating..." : "Start Animation"}
      </button>
      
      <div ref={scope} className="flex gap-4">
        <motion.div
          id="circle-1"
          initial={{ y: 50, scale: 0.5 }}
          className="h-12 w-12 rounded-full bg-blue-500"
        />
        <motion.div
          id="circle-2"
          initial={{ y: 50, scale: 0.5 }}
          className="h-12 w-12 rounded-full bg-green-500"
        />
        <motion.div
          id="circle-3"
          initial={{ y: 50, scale: 0.5 }}
          className="h-12 w-12 rounded-full bg-red-500"
        />
      </div>
    </div>
  );
};
Multiple Elements Example

Single Element with useAnimation

When you need to control just one element, use useAnimation. This gives you direct control over a single motion component. Perfect for button interactions or simple single-element animations:
import { motion, useAnimation } from "framer-motion";
import { useState } from "react";
import { SendHorizontal } from "lucide-react";

const SingleElementExample = () => {
  const controls = useAnimation();
  const [isAnimating, setIsAnimating] = useState(false);

  const handleClick = async () => {
    if (isAnimating) return;

    setIsAnimating(true);

    // Step 1: Scale down and move to center
    await controls.start({
      scale: 0.8,
      x: 0,
      transition: { duration: 0.18, ease: "easeInOut" },
    });

    // Step 2: Move to the right (like sending)
    await controls.start({
      scale: 0.8,
      x: 100,
      transition: { duration: 0.28, ease: "easeInOut" },
    });

    // Step 3: Reset position off-screen (invisible)
    await controls.set({ scale: 1, x: -100 });

    // Step 4: Animate back to center (return)
    await controls.start({
      scale: 1,
      x: 0,
      transition: { duration: 0.28, ease: "easeInOut" },
    });
    
    setIsAnimating(false);
  };

  return (
    <div className="flex flex-col items-center gap-4">
      <button
        onClick={handleClick}
        disabled={isAnimating}
        className="rounded-lg bg-green-500 px-4 py-2 text-white disabled:opacity-50"
      >
        {isAnimating ? "Animating..." : "Send Animation"}
      </button>
      
      <div className="border-foreground/20 flex size-20 items-center justify-center overflow-hidden rounded-2xl border">
        <motion.span 
          id="send-icon"
          animate={controls} 
          style={{ willChange: "transform" }}
        >
          <SendHorizontal className="size-5" />
        </motion.span>
      </div>
    </div>
  );
};
Single Element Example (Click to animate)
Go Back