Skip to main content

r3f-animation

React Three Fiber animation - useFrame, useAnimations, spring physics, keyframes. Use when animating objects, playing GLTF animations, creating procedural motion, or implementing physics-based movement.

Source facts

Repository
zebbern/claude-code-guide
Last source activity
May 31, 2026 at 15:44
Detected SKILL.md language
English
Stars
4,664
Forks
470

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
r3f-animation
description
React Three Fiber animation - useFrame, useAnimations, spring physics, keyframes. Use when animating objects, playing GLTF animations, creating procedural motion, or implementing physics-based movement.
# React Three Fiber Animation ## 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 Hook The core animation hook in R3F. Runs every frame. ### Basic Usage ```tsx import { useFrame } from '@react-three/fiber' import { useRef } from 'react' function AnimatedMesh() { const meshRef = useRef() useFrame((state, delta) => { // state contains: clock, camera, scene, gl, mouse, etc. // delta is time since last frame in seconds meshRef.current.rotation.y += delta }) return ( <mesh ref={meshRef}> <boxGeometry /> <meshStandardMaterial color="orange" /> </mesh> ) } ``` ### State Object ```tsx useFrame((state, delta, xrFrame) => { const { clock, // THREE.Clock camera, // Current camera scene, // Scene gl, // WebGLRenderer mouse, // Normalized mouse position (-1 to 1) pointer, // Same as mouse viewport, // Viewport dimensions size, // Canvas size raycaster, // Raycaster get, // Get current state set, // Set state invalidate, // Request re-render (when frameloop="demand") } = state // Time-based animation const t = clock.getElapsedTime() meshRef.current.position.y = Math.sin(t) * 2 }) ``` ### Render Priority ```tsx // Lower numbers run first. Default is 0. // Use negative for pre-render, positive for post-render function PreRender() { useFrame(() => { // Runs before main render }, -1) } function PostRender() { useFrame(() => { // Runs after main render }, 1) } function DefaultRender() { useFrame(() => { // Runs at default priority (0) }) } ``` ### Conditional Animation ```tsx function ConditionalAnimation({ isAnimating }) { const meshRef = useRef() useFrame((state, delta) => { if (!isAnimating) return meshRef.current.rotation.y += delta }) return <mesh ref={meshRef}>...</mesh> } ``` ## GLTF Animations with useAnimations The recommended way to play animations from GLTF/GLB files. ### Basic Usage ```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(() => { // Play first animation actions[names[0]]?.play() }, [actions, names]) return <primitive ref={group} object={scene} /> } ``` ### Animation Control ```tsx function Character() { const group = useRef() const { scene, animations } = useGLTF('/models/character.glb') const { actions, mixer } = useAnimations(animations, group) useEffect(() => { const action = actions['Walk'] if (action) { // Playback control action.play() action.stop() action.reset() action.paused = true // Speed action.timeScale = 1.5 // 1.5x speed action.timeScale = -1 // Reverse // Loop modes action.loop = THREE.LoopOnce action.loop = THREE.LoopRepeat action.loop = THREE.LoopPingPong action.repetitions = 3 action.clampWhenFinished = true // Weight (for blending) action.weight = 1 } }, [actions]) return <primitive ref={group} object={scene} /> } ``` ### Crossfade Between Animations ```tsx import { useGLTF, useAnimations } from '@react-three/drei' import { useState, useEffect, useRef } from 'react' function Character() { const group = useRef() const { scene, animations } = useGLTF('/models/character.glb') const { actions } = useAnimations(animations, group) const [currentAnim, setCurrentAnim] = useState('Idle') useEffect(() => { // Fade out all animations Object.values(actions).forEach(action => { action?.fadeOut(0.5) }) // Fade in current animation actions[currentAnim]?.reset().fadeIn(0.5).play() }, [currentAnim, actions]) return ( <group ref={group}> <primitive object={scene} /> </group> ) } ``` ### Animation Events ```tsx function AnimatedModel() { const group = useRef() const { scene, animations } = useGLTF('/models/character.glb') const { actions, mixer } = useAnimations(animations, group) useEffect(() => { // Listen for animation events 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]) return <primitive ref={group} object={scene} /> } ``` ### Animation Blending ```tsx function CharacterController({ speed = 0 }) { const group = useRef() const { scene, animations } = useGLTF('/models/character.glb') const { actions } = useAnimations(animations, group) useEffect(() => { // Start all animations actions['Idle']?.play() actions['Walk']?.play() actions['Run']?.play() }, [actions]) // Blend based on speed 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) } }) return <primitive ref={group} object={scene} /> } ``` ## Spring Animation (@react-spring/three) Physics-based spring animations that integrate with R3F. ### Installation ```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> ) } ``` ### Spring Config Presets ```tsx import { useSpring, animated, config } from '@react-spring/three' function SpringPresets() { const { position } = useSpring({ position: [0, 2, 0], config: config.wobbly // Presets: default, gentle, wobbly, stiff, slow, molasses }) // Or custom config const { rotation } = useSpring({ rotation: [0, Math.PI, 0], config: { mass: 1, tension: 170, friction: 26, clamp: false, precision: 0.01, velocity: 0, } }) return ( <animated.mesh position={position} rotation={rotation}> <boxGeometry /> <meshStandardMaterial /> </animated.mesh> ) } ``` ### 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) => { if (i === index) return { scale: 1.5 } return { 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> ) } ``` ### Chain 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), then rotation (0.5-1) useChain([scaleRef, rotationRef], [0, 0.5]) return ( <animated.mesh scale={scale} rotation={rotation}> <boxGeometry />
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub