Three.js Reference
Three.js renders interactive 3D scenes. Generated apps use it through React Three Fiber, with Drei for common helpers and React Three Rapier when objects need 3D rigid-body physics.
Use React Three Fiber for almost every 3D app. It creates and updates Three.js objects through React components. Use Matter.js for a 2D game, and p5.js for custom 2D drawing or creative coding.
Available packages:
three@react-three/fiber@react-three/drei@react-three/rapier
Imports
Only these package roots are available:
import * as THREE from "three";
import {
Canvas,
useFrame,
useThree,
type ThreeElements,
} from "@react-three/fiber";
import {
KeyboardControls,
OrbitControls,
useGLTF,
useKeyboardControls,
useTexture,
} from "@react-three/drei";
import {
BallCollider,
CuboidCollider,
Physics,
RigidBody,
useBeforePhysicsStep,
type RapierRigidBody,
} from "@react-three/rapier";
Package subpaths such as three/addons/..., three/examples/jsm/..., and @react-three/fiber/native are unavailable. Dynamic import() is unavailable. Drei already provides the common controls and loaders generated apps need.
Choose the layer that owns the behavior:
- React Three Fiber owns scene rendering, animation, cameras, lights, pointer interaction, and reusable 3D components.
threesupplies classes, constants, math, colors, vectors, curves, and types used by Fiber code.- Drei supplies controls, loaders, reusable shapes, HTML overlays, instancing, bounds, and performance helpers.
- React Three Rapier owns objects that collide, fall, bounce, trigger sensors, or use joints.
Fiber already owns the renderer, scene, camera, and animation loop inside <Canvas>. A second THREE.WebGLRenderer inside that canvas fights the one Fiber created.
Canvas and scene setup
<Canvas> fills its parent, so the parent needs a real height. A zero-height parent produces a blank scene.
This Three.js release requires WebGL 2. Pass fallback so unsupported devices show a useful message.
import * as React from "react";
import * as THREE from "three";
import { Canvas, useFrame, type ThreeElements } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
function SpinningBox(props: ThreeElements["mesh"]) {
const meshRef = React.useRef<THREE.Mesh>(null);
const [active, setActive] = React.useState(false);
useFrame((_state, delta) => {
if (!meshRef.current) return;
meshRef.current.rotation.y += delta;
});
return (
<mesh
{...props}
ref={meshRef}
scale={active ? 1.25 : 1}
onClick={(event) => {
event.stopPropagation();
setActive((value) => !value);
}}
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={active ? "#f97316" : "#2563eb"} />
</mesh>
);
}
export default function App() {
return (
<main className="h-screen bg-zinc-950">
<Canvas
camera={{ position: [3, 2, 5], fov: 50 }}
dpr={[1, 1.5]}
fallback={
<div className="grid h-full place-items-center text-white">
WebGL 2 is unavailable on this device.
</div>
}
>
<color attach="background" args={["#09090b"]} />
<ambientLight intensity={0.6} />
<directionalLight position={[4, 6, 3]} intensity={2} />
<SpinningBox />
<gridHelper
args={[10, 10, "#52525b", "#27272a"]}
position={[0, -1, 0]}
/>
<OrbitControls
makeDefault
enableDamping
minDistance={2}
maxDistance={10}
/>
</Canvas>
</main>
);
}
Fiber JSX maps to Three.js constructors:
<mesh>createsTHREE.Mesh.<boxGeometry args={[1, 2, 3]}>passes those values tonew THREE.BoxGeometry(1, 2, 3).<meshStandardMaterial color="#2563eb">creates a material and sets its color.- Positions, scales, and Euler rotations accept arrays such as
[x, y, z]. - Rotation values use radians.
Fiber hooks
Fiber hooks run inside a component rendered below <Canvas>. They do not run in the component that returns the canvas.
useFrame
useFrame(callback, priority?) runs before Fiber renders a frame. The callback receives the Fiber state and the elapsed seconds since the previous frame.
useFrame((state, delta) => {
if (!meshRef.current) return;
meshRef.current.rotation.y += delta;
meshRef.current.position.y = Math.sin(state.clock.elapsedTime) * 0.25;
});
Mutate Three.js refs for per-frame motion. React state is for score, pause, selected level, menus, and other coarse UI. Calling a state setter every frame re-renders the scene continuously.
useThree
Select the value the component needs so it subscribes only to that slice:
const camera = useThree((state) => state.camera);
const viewport = useThree((state) => state.viewport);
const invalidate = useThree((state) => state.invalidate);
Common state includes gl, scene, camera, raycaster, pointer, clock, size, viewport, invalidate, and performance.
Pointer interaction
Meshes, lines, points, and other objects with a Three.js raycast method accept Fiber pointer events:
<mesh
onClick={(event) => {
event.stopPropagation();
setSelected(true);
}}
onPointerOver={(event) => {
event.stopPropagation();
document.body.style.cursor = "pointer";
}}
onPointerOut={() => {
document.body.style.cursor = "default";
}}
>
<sphereGeometry args={[0.6, 32, 16]} />
<meshStandardMaterial color={selected ? "#22c55e" : "#64748b"} />
</mesh>
Intersections travel through objects in distance order, including objects that are not parent and child. Call event.stopPropagation() when the front object should block objects behind it. Reset a changed cursor in onPointerOut or effect cleanup.
Controls and keyboard input
OrbitControls provides orbit, zoom, and pan. makeDefault registers it as the scene controls. Disable pan or zoom when a game should reserve those gestures.
For game input, define a stable keyboard map outside the component:
import {
KeyboardControls,
useKeyboardControls,
type KeyboardControlsEntry,
} from "@react-three/drei";
type Control = "forward" | "back" | "left" | "right" | "jump";
const KEYBOARD_MAP: KeyboardControlsEntry<Control>[] = [
{ name: "forward", keys: ["ArrowUp", "KeyW"] },
{ name: "back", keys: ["ArrowDown", "KeyS"] },
{ name: "left", keys: ["ArrowLeft", "KeyA"] },
{ name: "right", keys: ["ArrowRight", "KeyD"] },
{ name: "jump", keys: ["Space"] },
];
function PlayerController() {
const [, getKeys] = useKeyboardControls<Control>();
useFrame((_state, delta) => {
const { forward, back, left, right } = getKeys();
const x = Number(right) - Number(left);
const z = Number(back) - Number(forward);
// Apply x, z, and delta to the visual object or physics body.
});
return null;
}
export default function App() {
return (
<KeyboardControls map={KEYBOARD_MAP}>
<Canvas>
<PlayerController />
</Canvas>
</KeyboardControls>
);
}
Read getKeys inside useFrame so key transitions do not re-render React. Use a selector such as useKeyboardControls((state) => state.jump) when one control should update the rendered UI.
Loading assets
Generated apps load exact platform-hosted asset URLs supplied in the app context. Arbitrary external model, texture, HDR, font, and decoder URLs are blocked.
Fiber and Drei loader hooks suspend while loading. Place the model or texture component inside <React.Suspense>.
function Model({ url }: { url: string }) {
// false disables Drei's default third-party Draco decoder URL.
const gltf = useGLTF(url, false);
return <primitive object={gltf.scene} />;
}
function Scene({ modelUrl }: { modelUrl: string }) {
return (
<React.Suspense fallback={null}>
<Model url={modelUrl} />
</React.Suspense>
);
}
useGLTF(url, false) loads ordinary .glb or .gltf files. Drei enables Draco decoding by default and otherwise fetches a decoder from a third-party host, which the sandbox blocks. Draco-compressed models are unsupported.
Prefer a .glb with embedded textures. A .gltf may reference sidecar .bin and image files, and every referenced URL must also be platform-hosted and reachable relative to the model.
Load a platform-hosted image with useTexture:
function TexturedPlane({ imageUrl }: { imageUrl: string }) {
const texture = useTexture(imageUrl);
React.useEffect(() => {
texture.colorSpace = THREE.SRGBColorSpace;
texture.needsUpdate = true;
}, [texture]);
return (
<mesh>
<planeGeometry args={[3, 2]} />
<meshBasicMaterial map={texture} toneMapped={false} />
</mesh>
);
}
Drei Environment presets fetch HDR files from a third-party CDN. An Environment with no files, map, preset, or children also tries to load six same-origin cube-map files that generated apps do not provide.
The default Drei Text font fetches external font data. Use regular HTML for labels. When 3D text is essential, pass an explicit platform-hosted font URL and a characters string that covers every rendered glyph. A missing glyph can trigger the blocked fallback service.
Rapier physics
<Physics> creates and advances the Rapier world. Rapier initializes its WASM module lazily, so wrap Physics in <React.Suspense>.
Rapier's compat package embeds its WASM in the generated JavaScript. A representative app adds roughly 3.5 MB of minified output, so use Rapier when the app needs real 3D collisions or dynamics.
import * as React from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import { Physics, RigidBody } from "@react-three/rapier";
function PhysicsScene() {
return (
<React.Suspense fallback={null}>
<Physics gravity={[0, -9.81, 0]}>
<RigidBody position={[0, 3, 0]} colliders="ball" restitution={0.7} ccd>
<mesh castShadow>
<sphereGeometry args={[0.5, 32, 16]} />
<meshStandardMaterial color="#f97316" />
</mesh>
</RigidBody>
<RigidBody type="fixed" colliders="cuboid">
<mesh position={[0, -0.25, 0]} receiveShadow>
<boxGeometry args={[8, 0.5, 8]} />
<meshStandardMaterial color="#3f3f46" />
</mesh>
</RigidBody>
</Physics>
</React.Suspense>
);
}
export default function App() {
return (
<main className="h-screen bg-zinc-950">
<Canvas shadows camera={{ position: [6, 4, 7], fov: 50 }} dpr={[1, 1.5]}>
<ambientLight intensity={0.5} />
<directionalLight
castShadow
position={[4, 8, 4]}
intensity={2}
shadow-mapSize={[1024, 1024]}
/>
<PhysicsScene />
<OrbitControls makeDefault />
</Canvas>
</main>
);
}
Body types and colliders
Rigid body types:
"dynamic"is affected by forces and collisions. This is the default."fixed"stays in place."kinematicPosition"moves to positions supplied by the app."kinematicVelocity"moves from a velocity supplied by the app.
Automatic collider values are "cuboid", "ball", "hull", "trimesh", or false. Prefer cuboid, ball, and capsule colliders. Hull and trimesh colliders cost more, and trimesh colliders on moving bodies are a poor fit.
Custom collider dimensions use half extents:
<RigidBody colliders={false} position={[0, 2, 0]}>
<mesh>
<boxGeometry args={[2, 1, 3]} />
<meshStandardMaterial color="#2563eb" />
</mesh>
<CuboidCollider args={[1, 0.5, 1.5]} />
</RigidBody>
Moving bodies
Control a body through its Rapier ref. Animating a dynamic body's child mesh position in useFrame separates the visual from the collider.
function Player() {
const bodyRef = React.useRef<RapierRigidBody>(null);
const jump = () => {
const body = bodyRef.current;
if (!body) return;
body.applyImpulse({ x: 0, y: 4, z: 0 }, true);
};
return (
<RigidBody
ref={bodyRef}
colliders="ball"
enabledRotations={[false, false, false]}
>
<mesh onClick={jump}>
<sphereGeometry args={[0.5, 24, 16]} />
<meshStandardMaterial color="#22c55e" />
</mesh>
</RigidBody>
);
}
The final boolean passed to applyImpulse, addForce, setLinvel, and setTranslation wakes a sleeping body.
For a position-based kinematic body, call setNextKinematicTranslation. Run simulation-coupled movement in useBeforePhysicsStep, below <Physics>, so it happens before Rapier advances the world:
const platformRef = React.useRef<RapierRigidBody>(null);
const [, getKeys] = useKeyboardControls<Control>();
useBeforePhysicsStep(() => {
const body = platformRef.current;
if (!body) return;
const { left, right } = getKeys();
const position = body.translation();
const step = 3 / 60;
body.setNextKinematicTranslation({
x: position.x + (Number(right) - Number(left)) * step,
y: position.y,
z: position.z,
});
});
Import useBeforePhysicsStep from @react-three/rapier. The 3 / 60 step matches Physics's default fixed 60 Hz timestep. If the app changes timeStep, match movement calculations to that step.
Collision events and sensors
<RigidBody
name="player"
onCollisionEnter={({ other }) => {
if (other.rigidBodyObject?.name === "goal") {
setCompleted(true);
}
}}
>
<mesh>{/* geometry and material */}</mesh>
</RigidBody>
A sensor reports an overlap without pushing objects apart:
<CuboidCollider
args={[1.5, 0.5, 1.5]}
sensor
onIntersectionEnter={({ other }) => {
if (other.rigidBodyObject?.name === "player") {
setCompleted(true);
}
}}
/>
Collision callbacks provide target, other, and collider or body references. onCollisionEnter also provides manifold and flipped. Guard React state updates so repeated physics events do not render more often than the UI needs.
Performance and limits
- Cap
dpr, for exampledpr={[1, 1.5]}, especially on mobile. - Enable shadows when they materially help the scene. Limit shadow-casting lights and shadow map size.
- Reuse geometries and materials for many matching objects. Use Drei
Instancesor Three.jsInstancedMeshfor large repeated sets. - Use simple physics colliders and let bodies sleep.
- Enable
ccdon small, fast bodies that would otherwise pass through thin colliders. - Use
deltafromuseFramefor frame-rate-independent visual movement. - Use
frameloop="demand"for static scenes that do not animate continuously. - Keep frame-loop values and physics objects in refs.
- Fiber disposes declaratively created scene objects when it removes them. Manually dispose geometries, materials, textures, render targets, and controls created imperatively.
- Objects passed through
<primitive>stay allocated until the app disposes them. Dispose owned resources when they are no longer used, and leave shared cached loader results in place while another component may still use them.
Persist scores, settings, unlocked levels, and compact level data with usePersistentItem. Rebuild runtime objects from that JSON. Three.js objects, Rapier objects, vectors, textures, scenes, and refs are not persistence data.
Sandbox limits:
- External model, texture, HDR, font, audio, and decoder URLs are blocked.
- External scripts and CDN package imports are blocked.
- Package subpath imports and dynamic imports are unavailable.
- Immersive WebXR sessions are unsupported. Generated apps run in a sandboxed iframe.
- Pointer, keyboard, Canvas, WebGL, and Web Audio APIs are available, subject to the browser and device.