React, SMIL + viewBox animation notes

Tiger AbrodiTiger Abrodi
3 min read

What is viewBox Animation?

viewBox = your camera lens

<svg viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="20" />
</svg>

The circle is always at coordinate (50,50) with radius 20. The circle never moves. But when you animate viewBox, you're changing what part of the coordinate space you can see.

The Camera Analogy

Think of it like a camera filming a stage:

Stage (infinite SVG canvas):
┌─────────────────────────────────┐
│     🎭        🎪        🎨      │
│                                 │
│  🎵     ⭐ (50,50)     🎯      │
│                                 │
│     🎸        🎺        🎹      │
└─────────────────────────────────┘

viewBox="0 0 100 100" = Camera shows top-left corner, 100×100 area:

Camera view:
┌─────────────┐
│ 🎭   🎪     │
│             │
│ 🎵   ⭐     │  ← Star is visible at center
│             │
└─────────────┘

viewBox="50 50 100 100" = Camera moves to show different area:

Camera view:
┌─────────────┐
│      ⭐  🎯 │  ← Same star, now at top-left!
│             │
│   🎺   🎹   │
│             │
└─────────────┘

The star never moved -> we just changed our view of it!

The React Problem

Here's the tricky part with React:

What you want to happen:

  1. viewBox starts at "0 0 100 100"

  2. User changes prop to "50 50 100 100"

  3. viewBox smoothly animates from old → new

What actually happens:

  1. React re-renders with new prop

  2. SVG immediately gets viewBox="50 50 100 100"

  3. animate element also gets to="50 50 100 100"

  4. You're trying to animate from "50 50 100 100" to "50 50 100 100"

  5. No animation!

The Solution Breakdown

function Viewbox({ viewbox, children }) {
  const previousViewbox = usePrevious(viewbox); // Remember old value
  const animateRef = useRef();

  useEffect(() => {
    if (previousViewbox) {
      // Skip first render
      animateRef.current?.beginElement(); // Start animation
    }
  }, [viewbox]);

  return (
    <svg viewBox={viewbox}>
      {" "}
      {/* React sets this immediately */}
      {children}
      <animate
        ref={animateRef}
        attributeName="viewBox"
        from={previousViewbox} // Old value
        to={viewbox} // New value
        dur="0.3s"
        begin="indefinite" // Don't auto-start
      />
    </svg>
  );
}

Step-by-Step Animation Flow

Initial state:

  • viewbox prop = "0 0 100 100"

  • previousViewbox = undefined

  • SVG shows: viewBox="0 0 100 100"

User changes to "50 50 100 100":

  1. React re-render:

    • viewbox prop = "50 50 100 100"

    • previousViewbox = "0 0 100 100" (from usePrevious)

    • SVG immediately shows: viewBox="50 50 100 100" (React controlled)

  2. animate element gets:

     <animate from="0 0 100 100" to="50 50 100 100" />
    
  3. useEffect runs:

     animateRef.current.beginElement(); // Start the animation
    
  4. SMIL animation takes over:

    • Overrides the SVG's viewBox attribute

    • Smoothly interpolates: "0 0 100 100" → "50 50 100 100"

    • Takes 0.3 seconds

  5. Animation ends:

    • SMIL stops controlling viewBox

    • SVG returns to React's value: "50 50 100 100"

    • Seamless handoff!

The Magic of usePrevious

function usePrevious(value) {
  const ref = useRef(value);

  useEffect(() => {
    ref.current = value; // Update AFTER render
  }, [value]);

  return ref.current; // Return OLD value during render
}

Timeline:

Render 1: value="A" → usePrevious returns "A"
Render 2: value="B" → usePrevious returns "A" (useEffect hasn't run yet)
          useEffect runs → ref.current becomes "B"
Render 3: value="C" → usePrevious returns "B"
          useEffect runs → ref.current becomes "C"

Visual Effects You Can Create

Zoom in:

// From wide view to focused view
viewBox="0 0 200 200" → viewBox="75 75 50 50"

Pan across:

// Slide the camera right
viewBox="0 0 100 100" → viewBox="50 0 100 100"

Zoom out:

// From detail to overview
viewBox="40 40 20 20" → viewBox="0 0 100 100"

Follow an object:

// Keep character centered as they move
character at (30,40) → viewBox="5 15 50 50"
character at (70,20) → viewBox="45 -5 50 50"

Why This is Powerful

  1. Performance: No DOM elements move -> just the viewport

  2. Simplicity: One attribute controls complex camera movements

  3. Flexibility: Works with any SVG content

  4. Smoothness: Native browser interpolation

0
Subscribe to my newsletter

Read articles from Tiger Abrodi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tiger Abrodi
Tiger Abrodi

Just a guy who loves to write code and watch anime.