React, SMIL + viewBox animation notes


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:
viewBox starts at "0 0 100 100"
User changes prop to "50 50 100 100"
viewBox smoothly animates from old → new
What actually happens:
React re-renders with new prop
SVG immediately gets
viewBox="50 50 100 100"
animate element also gets
to="50 50 100 100"
You're trying to animate from "50 50 100 100" to "50 50 100 100"
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
= undefinedSVG shows:
viewBox="0 0 100 100"
User changes to "50 50 100 100":
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)
animate element gets:
<animate from="0 0 100 100" to="50 50 100 100" />
useEffect runs:
animateRef.current.beginElement(); // Start the animation
SMIL animation takes over:
Overrides the SVG's viewBox attribute
Smoothly interpolates: "0 0 100 100" → "50 50 100 100"
Takes 0.3 seconds
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
Performance: No DOM elements move -> just the viewport
Simplicity: One attribute controls complex camera movements
Flexibility: Works with any SVG content
Smoothness: Native browser interpolation
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.