Navigate
Search topics across all sections
Animation Libraries
A 3D scene without animation is a statue garden. Beautiful, sure, but lifeless. Animation is what makes objects feel alive -- a box that bounces when clicked, a camera that smoothly glides to a new viewpoint, a character that breathes. Let us look at three fundamentally different ways to animate in R3F and understand when to reach for each one.
You animate a menu by setting React state 60 times per second inside useFrame. Each setState triggers a full component re-render. Your scene grinds to a halt because React is reconciling the virtual DOM on every single frame while also trying to render 3D graphics.
Warning: Maximum update depth exceeded. React has detected setState being called inside useFrame at 60fps. Component re-rendered 3,847 times in the last second. Frame time: 142ms (7 FPS). Expected: 16ms (60 FPS).
Real-world
Think of animation as choreography. You have three choreographers, each with a different style.
The first is a flipbook artist. They draw every single frame by hand -- position at frame 1, position at frame 2, position at frame 3. Total control, but you do all the work. That is useFrame.
The second is a rubber band. You pull it to a new position and let go. It overshoots, bounces back, and settles naturally. You did not plan each frame -- you just described the destination and the rubber band figured out the journey. That is spring physics.
The third is a GPS navigation. "Drive from A to B in exactly 2 seconds, easing in at the start and out at the end." Precise, predictable, no surprises. That is lerp with easing.
Choosing Your Approach
Need full control?
useFrame with manual math
Want organic feel?
Spring physics (stiffness + damping)
Need exact timing?
Lerp with easing curves
Combine them
useFrame drives everything -- springs and lerps run inside it
See It In Action
Three boxes, three animation styles. Click each one to toggle its position. The red box uses raw useFrame -- it just teleports. The blue box uses spring physics -- notice how it overshoots and bounces. The green box uses lerp -- smooth and predictable. Try adjusting stiffness, damping, and smoothness in the controls.
Building It Step by Step
Step 1 -- Raw animation with useFrame
useFrame gives you a callback that runs every frame. You get the clock state and delta time. Mutate refs directly -- never use setState here.
import { useFrame } from "@react-three/fiber"
import { useRef } from "react"
function SpinningBox() {
const ref = useRef<THREE.Mesh>(null)
useFrame((state, delta) => {
if (!ref.current) return
// Rotate a little each frame
ref.current.rotation.y += delta * 0.5
// Bob up and down with a sine wave
ref.current.position.y = Math.sin(state.clock.elapsedTime) * 0.3
})
return (
<mesh ref={ref}>
<boxGeometry />
<meshStandardMaterial color="tomato" />
</mesh>
)
}Step 2 -- Spring physics (manual implementation)
A spring is defined by three values: stiffness (how hard it pulls), damping (how fast oscillations die), and mass (how heavy the object). The math is surprisingly simple.
const velocity = useRef(0)
const current = useRef(0)
useFrame((_, delta) => {
const target = toggled ? 2 : 0
const stiffness = 120
const damping = 8
const mass = 1
// Spring force: F = -k * displacement
const displacement = current.current - target
const springForce = -stiffness * displacement
const dampingForce = -damping * velocity.current
const acceleration = (springForce + dampingForce) / mass
velocity.current += acceleration * delta
current.current += velocity.current * delta
mesh.current.position.y = current.current
})High stiffness = snappy. High damping = no overshoot. High mass = sluggish. Play with the controls in the demo above to feel the difference.
Step 3 -- Smooth lerp interpolation
THREE.MathUtils.lerp(a, b, t) returns a value that is t percent of the way from a to b. Called every frame with a small t, it creates an exponential ease-out -- fast at first, slower as it approaches the target.
import * as THREE from "three"
useFrame(() => {
const target = toggled ? 2 : 0
mesh.current.position.y = THREE.MathUtils.lerp(
mesh.current.position.y, // current
target, // destination
0.05 // 5% closer each frame
)
})Lerp is dead simple but has a quirk: with a fixed factor, it technically never arrives (it just gets infinitely close). In practice, the difference becomes sub-pixel and invisible.
What you just learned
useFrame gives you per-frame control -- mutate refs directly, never setState
Spring physics use stiffness, damping, and mass to create organic, bouncy motion
Lerp creates smooth exponential ease-out by moving a percentage closer each frame
All three approaches run inside useFrame -- it is the engine that drives everything
Allocate objects (Vector3, etc.) outside useFrame to avoid garbage collection stutters
Question
When should I use a library like react-spring instead of manual spring math? If you need to animate React state (opacity, color, CSS transforms) alongside 3D properties, a library handles the coordination. For pure 3D animation inside useFrame, manual springs are often simpler and avoid an extra dependency.
Think about it...
You set lerp factor to 0.5 inside useFrame (no delta correction). On a 60Hz monitor your animation takes about 0.2 seconds. What happens on a 144Hz monitor?
Try These Challenges
Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.
Try This!
Beginner
Click all 3 boxes — watch spring vs lerp
Try This!
Beginner
Max stiffness — snappy spring
Try This!
Beginner
Set smoothness to 0.01 — instant lerp
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
useFrame(() => {
// New Vector3 every frame = garbage collection stutter
mesh.current.position.lerp(
new THREE.Vector3(targetX, targetY, targetZ),
0.1
)
})useFrame(() => {
// This re-renders the component every frame!
setPosition(mesh.current.position.y)
})useFrame(() => {
// 0.1 per frame: at 60fps that's different than 144fps
mesh.current.position.x = THREE.MathUtils.lerp(
mesh.current.position.x, target, 0.1
)
})Best Practices
Mutate refs, not state
Inside useFrame, always work with refs and Three.js objects directly. React state triggers re-renders -- that is the opposite of what you want at 60fps.
Use delta for consistency
Always multiply movement by delta from useFrame. This ensures your animation runs at the same speed regardless of the monitor refresh rate.
Pre-allocate reusable objects
Create Vector3, Quaternion, and other temporary objects once in a ref or outside the component. Reuse them inside useFrame to avoid garbage collection hitches.
Springs for interaction, lerp for cameras
Springs feel great for user-triggered actions (click, drag) because the overshoot gives tactile feedback. Lerp is better for camera movement where overshoot feels nauseating.