Home
cd ../playbooks
Developer ToolsIntermediate

React Three Fiber Animation Reference

Animate React Three Fiber scenes correctly — useFrame fundamentals with render priority, full GLTF animation control via useAnimations (playback, crossfade, event listeners, speed-based blending between Idle/Walk/Run), @react-spring/three physics animations with gesture and chain integration, and the five performance rules that keep per-frame updates from triggering React re-renders.

10 minutes
By zebbern (claude-code-guide)Source
#react-three-fiber#threejs#react#3d-animation#webgl#react-spring

Calling setState inside a React Three Fiber useFrame callback re-renders the entire React component tree sixty times a second — the fix (mutate the mesh ref directly instead) is one line, but it's the single most common performance mistake in every R3F codebase that started as a normal React app.

Who it's for: React developers building 3D scenes and animations with React Three Fiber, engineers implementing character animation blending (Idle/Walk/Run) from GLTF models, teams choosing between useFrame manual animation and @react-spring/three physics-based springs, anyone debugging janky R3F performance caused by React re-renders inside the animation loop

Example

"Blend between idle, walk, and run animations based on character speed" → useAnimations wired to a GLTF's action clips, all three animations started simultaneously, and their effective weights cross-blended every frame inside useFrame based on a speed threshold — the correct GLTF animation-blending pattern instead of abruptly switching between separate play() calls

CLAUDE.md Template

New here? 3-minute setup guide → | Already set up? Copy the template below.

# React Three Fiber Animation

Animate objects in React Three Fiber (R3F) — `useFrame`, GLTF animations with `useAnimations`, spring physics with `@react-spring/three`, morph targets, skeletal animation, procedural motion, and physics-based movement.

## Quick Start

```tsx
import { Canvas, useFrame } from '@react-three/fiber'
import { useRef } from 'react'

function RotatingBox() {
  const meshRef = useRef()

  useFrame((state, delta) => {
    meshRef.current.rotation.x += delta
    meshRef.current.rotation.y += delta * 0.5
  })

  return (
    <mesh ref={meshRef}>
      <boxGeometry />
      <meshStandardMaterial color="hotpink" />
    </mesh>
  )
}

export default function App() {
  return (
    <Canvas>
      <ambientLight />
      <RotatingBox />
    </Canvas>
  )
}
```

## useFrame — The Core Animation Hook

Runs every frame. The `state` object carries `clock`, `camera`, `scene`, `gl`, `mouse`/`pointer`, `viewport`, `size`, `raycaster`, `get`/`set`, and `invalidate` (for `frameloop="demand"`):

```tsx
useFrame((state, delta) => {
  const t = state.clock.getElapsedTime()
  meshRef.current.position.y = Math.sin(t) * 2
})
```

**Render priority** — lower numbers run first; default is 0. Use negative for pre-render, positive for post-render: `useFrame(() => {...}, -1)`.

**Conditional animation** — gate the update inside the callback rather than conditionally calling the hook:

```tsx
useFrame((state, delta) => {
  if (!isAnimating) return
  meshRef.current.rotation.y += delta
})
```

## GLTF Animations with useAnimations

The recommended way to play animations from GLTF/GLB files.

```tsx
import { useGLTF, useAnimations } from '@react-three/drei'
import { useEffect, useRef } from 'react'

function AnimatedModel() {
  const group = useRef()
  const { scene, animations } = useGLTF('/models/character.glb')
  const { actions, names } = useAnimations(animations, group)

  useEffect(() => {
    actions[names[0]]?.play()
  }, [actions, names])

  return <primitive ref={group} object={scene} />
}
```

### Animation Control

```tsx
const action = actions['Walk']
action.play()
action.stop()
action.reset()
action.paused = true

action.timeScale = 1.5   // 1.5x speed
action.timeScale = -1    // reverse

action.loop = THREE.LoopOnce
action.loop = THREE.LoopRepeat
action.loop = THREE.LoopPingPong
action.repetitions = 3
action.clampWhenFinished = true

action.weight = 1        // for blending
```

### Crossfade Between Animations

```tsx
useEffect(() => {
  Object.values(actions).forEach(action => action?.fadeOut(0.5))
  actions[currentAnim]?.reset().fadeIn(0.5).play()
}, [currentAnim, actions])
```

### Animation Events

```tsx
useEffect(() => {
  const onFinished = (e) => console.log('Animation finished:', e.action.getClip().name)
  const onLoop = (e) => console.log('Animation looped:', e.action.getClip().name)
  mixer.addEventListener('finished', onFinished)
  mixer.addEventListener('loop', onLoop)
  return () => {
    mixer.removeEventListener('finished', onFinished)
    mixer.removeEventListener('loop', onLoop)
  }
}, [mixer])
```

### Animation Blending (Idle/Walk/Run by Speed)

```tsx
useEffect(() => {
  actions['Idle']?.play()
  actions['Walk']?.play()
  actions['Run']?.play()
}, [actions])

useFrame(() => {
  if (speed < 0.1) {
    actions['Idle']?.setEffectiveWeight(1)
    actions['Walk']?.setEffectiveWeight(0)
    actions['Run']?.setEffectiveWeight(0)
  } else if (speed < 5) {
    const t = speed / 5
    actions['Idle']?.setEffectiveWeight(1 - t)
    actions['Walk']?.setEffectiveWeight(t)
    actions['Run']?.setEffectiveWeight(0)
  } else {
    const t = Math.min((speed - 5) / 5, 1)
    actions['Idle']?.setEffectiveWeight(0)
    actions['Walk']?.setEffectiveWeight(1 - t)
    actions['Run']?.setEffectiveWeight(t)
  }
})
```

## Spring Animation (@react-spring/three)

Physics-based spring animations that integrate with R3F.

```bash
npm install @react-spring/three
```

### Basic Spring

```tsx
import { useSpring, animated } from '@react-spring/three'

function AnimatedBox() {
  const [active, setActive] = useState(false)
  const { scale, color } = useSpring({
    scale: active ? 1.5 : 1,
    color: active ? '#ff6b6b' : '#4ecdc4',
    config: { mass: 1, tension: 280, friction: 60 }
  })

  return (
    <animated.mesh scale={scale} onClick={() => setActive(!active)}>
      <boxGeometry />
      <animated.meshStandardMaterial color={color} />
    </animated.mesh>
  )
}
```

### Config Presets

```tsx
import { config } from '@react-spring/three'
// Presets: config.default, config.gentle, config.wobbly, config.stiff, config.slow, config.molasses

// Or fully custom:
{ mass: 1, tension: 170, friction: 26, clamp: false, precision: 0.01, velocity: 0 }
```

### Multiple Springs

```tsx
import { useSprings, animated } from '@react-spring/three'

function AnimatedBoxes({ count = 5 }) {
  const [springs, api] = useSprings(count, (i) => ({
    position: [i * 2 - count, 0, 0],
    scale: 1,
    config: { mass: 1, tension: 280, friction: 60 }
  }))

  const handleClick = (index) => {
    api.start((i) => (i === index ? { scale: 1.5 } : { scale: 1 }))
  }

  return springs.map((spring, i) => (
    <animated.mesh key={i} position={spring.position} scale={spring.scale} onClick={() => handleClick(i)}>
      <boxGeometry />
      <meshStandardMaterial color="orange" />
    </animated.mesh>
  ))
}
```

### Gesture Integration

```tsx
import { useSpring, animated } from '@react-spring/three'
import { useDrag } from '@use-gesture/react'

function DraggableBox() {
  const [spring, api] = useSpring(() => ({
    position: [0, 0, 0],
    config: { mass: 1, tension: 280, friction: 60 }
  }))

  const bind = useDrag(({ movement: [mx, my], down }) => {
    api.start({ position: down ? [mx / 100, -my / 100, 0] : [0, 0, 0] })
  })

  return (
    <animated.mesh {...bind()} position={spring.position}>
      <boxGeometry />
      <meshStandardMaterial color="hotpink" />
    </animated.mesh>
  )
}
```

### Chained Animations

```tsx
import { useSpring, animated, useChain, useSpringRef } from '@react-spring/three'

function ChainedAnimation() {
  const scaleRef = useSpringRef()
  const rotationRef = useSpringRef()

  const { scale } = useSpring({
    ref: scaleRef, from: { scale: 0 }, to: { scale: 1 },
    config: { tension: 200, friction: 20 }
  })
  const { rotation } = useSpring({
    ref: rotationRef, from: { rotation: [0, 0, 0] }, to: { rotation: [0, Math.PI * 2, 0] },
    config: { tension: 100, friction: 30 }
  })

  // Scale first (0-0.5 of the chain), then rotation (0.5-1)
  useChain([scaleRef, rotationRef], [0, 0.5])

  return (
    <animated.mesh scale={scale} rotation={rotation}>
      <boxGeometry />
      <meshStandardMaterial color="cyan" />
    </animated.mesh>
  )
}
```

## Performance Tips

1. **Isolate animated components** — split animated meshes into their own components so a parent's re-render doesn't cascade into every child; only the animated mesh should re-render.
2. **Use refs over state** — mutate `mesh.current.rotation`/`position` directly inside `useFrame` instead of `setState`, which would trigger a full React re-render every frame.
3. **Throttle expensive calculations** — accumulate `delta` and only run costly logic every N milliseconds, while still updating cheap properties every frame:

```tsx
function ThrottledAnimation() {
  const meshRef = useRef()
  const accumulated = useRef(0)

  useFrame((state, delta) => {
    accumulated.current += delta
    if (accumulated.current > 0.1) {
      // expensive calculation here, throttled to every 100ms
      accumulated.current = 0
    }
    meshRef.current.rotation.y += delta // cheap, runs every frame
  })
}
```

4. **Pause offscreen animations** — check visibility/frustum before running expensive per-frame updates on something the camera can't see.
5. **Share animation clips** — reuse the same loaded clip across multiple instances instead of re-loading or re-parsing per instance.

## When NOT to Use This

General Three.js animation outside React (use vanilla Three.js patterns), CSS/DOM animation for non-3D UI elements, or 2D canvas animation unrelated to a WebGL scene.

Get new playbooks like this one

One email a week with new Claude Code workflows. Free, like everything here.

No spam. Unsubscribe anytime.

README.md

What This Does

A working code reference for animating React Three Fiber scenes across every common technique: useFrame fundamentals (the per-frame state object, render priority ordering via the second argument, and conditional animation done correctly inside the callback rather than conditionally calling the hook), full GLTF character animation via useAnimations — playback control (play/stop/reset/pause/speed/loop mode), crossfading between clips, listening for finished/loop mixer events, and the speed-based weight-blending pattern that smoothly transitions between Idle, Walk, and Run clips instead of abruptly switching between them.

The spring-physics half covers @react-spring/three from basic single-value springs through multi-spring arrays, gesture-driven dragging via @use-gesture/react, and chained animations (useChain) that sequence one spring after another with precise timing overlap. It closes with five concrete performance rules — isolate animated components so re-renders don't cascade, mutate refs instead of calling setState inside useFrame, throttle expensive per-frame calculations via delta accumulation while keeping cheap updates running every frame, pause offscreen animations, and share animation clips across instances — the exact fixes for the performance mistakes that show up first in a normal React developer's initial R3F code.


Quick Start

Step 1: Create a Project Folder

mkdir r3f-animation && cd r3f-animation

Step 2: Download the Template

Click Download above, then:

mv ~/Downloads/CLAUDE.md ./

Step 3: Animate a Scene

claude

Ask Claude to animate an object, play a GLTF character animation, add a spring-based hover or drag interaction, or fix janky performance in an existing R3F scene. It will use useFrame or @react-spring/three correctly for the situation and apply the performance rules by default.


Tips & Best Practices

  • Default to mutating refs inside useFrame rather than useState for anything animating every frame — treat a setState call inside the animation loop as a performance bug to catch in review, not a stylistic choice.
  • For character animation, prefer the weight-blending pattern (all relevant clips playing simultaneously with setEffectiveWeight cross-fading between them) over abrupt play()/stop() switches — it's what produces smooth transitions between states like idle and walking.
  • Reach for @react-spring/three specifically when an animation needs to be interruptible mid-motion (a drag gesture, a hover that can re-trigger before finishing) — springs naturally handle interruption in a way fixed-duration useFrame tweening doesn't.

Limitations

  • Covers React Three Fiber and Drei/react-spring integration specifically — vanilla Three.js animation patterns outside React aren't the same API surface, even though the underlying concepts (mixers, action weights, clips) are shared.
  • GLTF animation examples assume the model was exported with named animation clips; a model without clean clip names will need those identified first (e.g. by logging names from useAnimations).
  • Performance guidance targets typical scene complexity; a scene with very large numbers of simultaneously animated instances may need additional techniques (instancing, GPU-driven animation) beyond what's covered here.

$Related Playbooks

Developer Tools

Reshape PR Commits

Turn a branch's messy WIP/fixup commit history into a clean, logically-grouped set of commits before review — proposed regrouping with explicit approval, git reset --soft plus staged re-commits instead of interactive rebase, and hard guardrails (feature branches only, force-with-lease only, never touch another author's commits).

5 minutes
Intermediate
Developer Tools

Planning with Files

Persistent, file-based planning for multi-step AI-agent work — task_plan.md, findings.md, and progress.md on disk, a 2-action rule for capturing multimodal findings before they're lost, a 3-strike error protocol, and a 5-question reboot test to verify state survives a compaction.

5 minutes
Intermediate
Developer Tools

PR Queue Triage

Clear a backlog of open pull requests before a release by classifying every PR into an evidence-based disposition — never by title — with a real git merge-tree test against the actual release branch, not the platform's often-wrong mergeable flag.

10 minutes
Intermediate
Developer Tools

Reproducible Database Lookup

A methodology for querying public database APIs — scientific, regulatory, financial, or otherwise — so another agent or human can repeat exactly what you did: bounded calls, count reconciliation, identifier-conversion tracking, and untrusted-data handling for every response.

10 minutes
Advanced
Developer Tools

Project Graveyard: Autopsy Your Abandoned Side Projects

Scan local repos for dead side projects, autopsy each one from its git history, surface your personal death patterns, and pick the one corpse most worth resurrecting — then help ship it.

10 minutes
Intermediate
Developer Tools

PR Review Toolkit

Six specialist reviewers — comments, tests, error handling, type design, general quality, and simplification — each triggered by name or automatically based on what changed in the diff.

5 minutes
Intermediate
Developer Tools

Ralph Wiggum Autonomous Loop

Self-referential development loop that keeps Claude iterating on the same task until it hits a completion promise or an iteration cap — a Stop hook that blocks exit and re-feeds the same prompt.

5 minutes
Advanced
Developer Tools

Redesign Existing Projects: UI Audit and Upgrade

A design audit checklist that finds generic AI-look patterns in an existing codebase and fixes them without breaking functionality or migrating frameworks

10 minutes
Intermediate
Developer Tools

Prompt Optimizer (EARS)

Transform vague prompts into precise, well-structured specifications using EARS (Easy Approach to Requirements Syntax) — ideal for AI-generated code, products, and docs.

10 minutes
Intermediate
Developer Tools

Promptfoo Evaluation

Configure and run LLM evaluations with Promptfoo — build promptfooconfig.yaml, write Python custom assertions, implement llm-rubric judges, and compare models systematically.

15 minutes
Intermediate
Developer Tools

QA Expert

Establish a comprehensive QA process — test strategies, Google-standard test cases, P0–P4 bug tracking, quality metrics, OWASP security testing, and 90% coverage targets.

20 minutes
Advanced
Developer Tools

Repomix Safe Mixer

Package codebases with repomix while automatically detecting and removing hardcoded credentials before packing — so you never leak secrets when sharing code.

5 minutes
Beginner

Browse all Developer Tools playbooks →