Animation in Three.js serves three purposes: communicating state changes, guiding attention, and making interactions feel responsive. A camera that snaps to a new position disorients the user. A camera that smoothly transitions over 400ms preserves spatial context. A data point that fades in from transparent communicates "this just arrived." One that pops into existence is jarring.
The engineering problem is not making things move. That is trivial. The problem is making things move well: within the frame budget, without jank, cancellable when interrupted, and appropriate for the device. A 60fps spring animation on a desktop GPU becomes a stuttering mess on a throttled mobile chip if you do not adapt.
The Constraint: Frame Budget and Interruption
Every animation runs inside the render loop. At 60fps, each frame has 16.67ms for everything: scene updates, animation calculations, draw calls, and GPU work. An animation that consumes 5ms per frame eats 30% of the budget. Two such animations running simultaneously leave no room for anything else.
The harder constraint is interruption. A user clicks a node to zoom in, then clicks another node before the first animation completes. The camera is now mid-transition to the wrong target. If the animation system cannot cancel, reverse, or redirect mid-flight, the interface feels unresponsive. Every animation must be interruptible.
The Naive Approach
Tutorial animation code works for demos. It creates problems at scale.
GSAP for Camera and Object Transitions
GSAP (GreenSock Animation Platform) is the standard for complex, interruptible animations. It handles easing, sequencing, timeline control, and cancellation. For Three.js, it animates object properties directly.
import gsap from 'gsap';
// Smooth camera transition to focus on an object
function zoomToTarget(camera, controls, targetPosition, duration = 0.6) {
// Kill any running camera animation first
gsap.killTweensOf(camera.position);
gsap.killTweensOf(controls.target);
const offset = new THREE.Vector3(2, 2, 4);
const destination = targetPosition.clone().add(offset);
gsap.to(camera.position, {
x: destination.x,
y: destination.y,
z: destination.z,
duration,
ease: 'power2.out',
onUpdate: () => camera.updateProjectionMatrix(),
});
gsap.to(controls.target, {
x: targetPosition.x,
y: targetPosition.y,
z: targetPosition.z,
duration,
ease: 'power2.out',
onUpdate: () => controls.update(),
});
}
The critical detail is gsap.killTweensOf() at the start. If the user clicks a new target before the previous transition completes, the old animation is cancelled instantly. Without this, animations pile up and the camera drifts erratically.
GSAP works with any numeric property on any object. Animate mesh.material.opacity for fades, mesh.scale.x for growth, mesh.rotation.y for turns. The easing library covers every common curve: power2.out for natural deceleration, elastic.out for bounce, back.out for overshoot.
Spring Physics
Springs model physical motion: an object accelerates toward a target, overshoots, oscillates, and settles. The result feels alive in a way that eased tweens cannot replicate. React Three Fiber applications use @react-spring/three for declarative spring animation.
import { useSpring, animated } from '@react-spring/three';
function DataPoint({ position, isSelected }) {
const { scale, color } = useSpring({
scale: isSelected ? 1.5 : 1,
color: isSelected ? '#4b93d6' : '#6b7280',
config: { mass: 1, tension: 280, friction: 20 },
});
return (
<animated.mesh position={position} scale={scale}>
<sphereGeometry args={[0.3, 16, 16]} />
<animated.meshStandardMaterial color={color} />
</animated.mesh>
);
}
Snappy (UI interactions)
High tension (280+), moderate friction (20). Selection highlights, button presses, tooltip appearances. Responds immediately, settles quickly.
Gentle (ambient motion)
Low tension (80-120), higher friction (30+). Background floating, idle camera drift, breathing effects. Slow, deliberate, never distracting.
Springs handle interruption naturally: if the target changes mid-animation, the spring redirects without discontinuity. No cancellation logic needed.
For vanilla Three.js (without React), implement a simple spring solver:
function createSpring(config = {}) {
const { stiffness = 200, damping = 20, mass = 1 } = config;
let velocity = 0;
let current = 0;
return {
update(target, delta) {
const force = stiffness * (target - current);
const dampForce = damping * velocity;
const acceleration = (force - dampForce) / mass;
velocity += acceleration * delta;
current += velocity * delta;
return current;
},
get value() { return current; },
set value(v) { current = v; velocity = 0; },
};
}
Skeletal Animation
GLTF models carry embedded skeletal animations: walk cycles, door openings, mechanical arm movements, assembly sequences. Three.js AnimationMixer plays, blends, and crossfades these clips.
const mixer = new THREE.AnimationMixer(model);
const clips = model.animations;
// Play a specific clip
const action = mixer.clipAction(clips.find(c => c.name === 'Open'));
action.setLoop(THREE.LoopOnce);
action.clampWhenFinished = true;
action.play();
// Crossfade between two animations over 0.3 seconds
function crossFade(fromAction, toAction, duration = 0.3) {
toAction.reset().play();
fromAction.crossFadeTo(toAction, duration);
}
// Update in the render loop
function animate(delta) {
mixer.update(delta);
}
Skeletal animation is relevant to product configurators (lid opening, drawer sliding, parts assembling), digital twins (valve rotation, conveyor belt motion), and any application with mechanical or character models.
Frame budget: mixer.update() traverses the bone hierarchy and updates transforms. For a single model with 30 bones, this is negligible (~0.1ms). For 50 animated models simultaneously, the cost adds up. Use LOD for animation: simplified clips or frozen poses for distant models.
Morph Targets
Morph targets (blend shapes) animate geometry between predefined shapes. A mesh smoothly deforms from one configuration to another by interpolating vertex positions. Use cases: facial expressions, terrain deformation, data-driven shape morphing.
// GLTF models with morph targets
const mesh = model.getObjectByName('terrain');
const influences = mesh.morphTargetInfluences;
// Animate between morph targets
gsap.to(influences, {
[0]: 0, // dry terrain
[1]: 1, // flooded terrain
duration: 1.5,
ease: 'power2.inOut',
});
Morph targets are GPU-friendly: the interpolation happens in the vertex shader. The CPU cost is setting the influence values (a handful of float assignments per frame). For data visualisation, morph targets can smoothly transition a surface plot between two datasets.
Data-Driven Animation
The most common animation pattern in Three.js applications is not decorative motion. It is data-driven: positions, colours, and sizes that change when data updates. The animated transition patterns in the data visualisation page cover buffer interpolation in detail.
Store from and to states
Snapshot the current buffer before applying new data. Interpolate between them over 300-500ms.
Use easing
easeOutCubic for arrivals (fast start, gentle stop). easeInOutCubic for bidirectional transitions. Never linear.
Stagger for readability
When hundreds of elements animate simultaneously, slight delays (5-10ms per element) create a wave that the eye can follow.
Handle interruption
If new data arrives mid-transition, restart from the current interpolated position, not from the original "from" state.
Motion Preferences and Accessibility
The prefers-reduced-motion media query is not optional. Users who set this preference may experience motion sickness, seizures, or vertigo from screen animation. Respect it.
const prefersReduced = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
// Adapt animation behaviour
const DURATION = prefersReduced ? 0 : 0.6;
const SPRING_CONFIG = prefersReduced
? { mass: 1, tension: 1000, friction: 100 } // instant
: { mass: 1, tension: 280, friction: 20 }; // springy
// For GSAP
gsap.defaults({ duration: DURATION });
// Listen for changes (user can toggle during session)
window.matchMedia('(prefers-reduced-motion: reduce)')
.addEventListener('change', (e) => {
gsap.defaults({ duration: e.matches ? 0 : 0.6 });
});
When reduced motion is active: camera transitions are instant (duration 0), spring animations settle immediately (high friction), data transitions snap without interpolation, ambient and decorative motion stops entirely. The information still updates. Only the motion is removed. See the accessibility page for broader guidance.
Performance Budgets for Animation
Animation cost compounds. Each running animation adds to the per-frame JavaScript cost. Budget accordingly.
| Animation type | Cost per frame | Budget at scale |
|---|---|---|
| GSAP tween (1 property) | ~0.01ms | Hundreds simultaneously |
| Spring solver (1 axis) | ~0.02ms | Hundreds simultaneously |
| Buffer interpolation (10K pts) | ~0.5ms | 2-3 concurrent transitions |
| AnimationMixer (30 bones) | ~0.1ms | 50 animated models |
| Morph target update | ~0.01ms (CPU) | GPU-bound, not CPU |
The rules: pre-allocate reusable Vector3 and Color objects (avoid creating temporaries in the render loop), use delta from the clock for time-based animation, and profile with Chrome DevTools Performance panel to verify frame budget compliance. For mobile optimisation, reduce concurrent animations, increase spring friction (fewer oscillation cycles), and shorten durations. On throttled devices, instant transitions are better than stuttering ones.
The Business Link
The difference: Animation is the difference between an application that feels like a tool and one that feels like a spreadsheet with a 3D viewport bolted on. Smooth camera transitions maintain spatial context. Data transitions communicate what changed. Spring physics make interactions feel responsive. Skeletal animation brings mechanical and product models to life.
The investment is in architecture (interruptible animation systems, motion preferences, frame budget awareness), not in making things wiggle. Well-engineered animation reduces cognitive load. Poorly-engineered animation causes motion sickness, wastes frame budget, and makes the application feel unreliable.
Build Responsive 3D Interfaces
We build Three.js applications with smooth, purposeful animation: camera transitions, data-driven motion, spring interactions, and skeletal sequences. Engineered for performance, interruptibility, and accessibility.
Let's talk about your project →