Animation Basics
Understanding Framer Motion Timeline BasicsWhen 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> ); };
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> ); };
Useful Links
- Framer Motion - Official documentation for animations
- Timeline Guide - How to move from GSAP to Framer Motion
- useAnimate hook from framer motion