Navigate

Search topics across all sections

GitHub
Animation & Physics

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.

terminal
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.

SpinningBox.tsxTSX
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.

SpringBox.tsxTSX
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.

SmoothBox.tsxTSX
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.

1
Creating new Vector3 objects every frame
Allocating memory 60 times per second inside useFrame
Don't do this
AnimatedMesh.tsxTSX
useFrame(() => {
  // New Vector3 every frame = garbage collection stutter
  mesh.current.position.lerp(
    new THREE.Vector3(targetX, targetY, targetZ),
    0.1
  )
})
Creating objects inside useFrame triggers garbage collection, causing micro-stutters. Allocate once outside the loop and reuse via a ref.
2
Using setState inside useFrame
Triggering a React re-render 60 times per second
Don't do this
AnimatedMesh.tsxTSX
useFrame(() => {
  // This re-renders the component every frame!
  setPosition(mesh.current.position.y)
})
setState inside useFrame causes 60 re-renders per second, destroying performance. useFrame is for direct mutation of refs and Three.js objects -- never React state.
3
Lerp factor that depends on frame rate
Animation runs faster on 144Hz monitors than 60Hz
Don't do this
AnimatedMesh.tsxTSX
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
  )
})
A fixed lerp factor like 0.1 means 'move 10% closer per frame.' On a 144Hz monitor that is 144 steps per second versus 60 on a standard display. Use delta to make the speed consistent across frame rates.

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.