src/skills/web-3d-react-three-fiber/SKILL.md
React Three Fiber (R3F) 3D rendering — Canvas, meshes, materials, lights, cameras, animations, events, physics, post-processing, performance
npx skillsauth add agents-inc/skills web-3d-react-three-fiberInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: R3F is a React renderer for Three.js. Every Three.js class maps to a JSX element (
<mesh>,<boxGeometry>,<meshStandardMaterial>). Use<Canvas>for scene setup,useFramefor per-frame logic (never setState inside it),useReffor direct mutations, anduseLoader/useGLTFfor assets. Animate via refs inuseFrame, not React state. Events work like DOM events with raycasting built in. Wrap exiting 3D components in<Suspense>for async asset loading.
Import:
import { Canvas, useFrame, useThree, useLoader } from "@react-three/fiber"
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST never call setState inside useFrame -- mutate refs directly for per-frame updates)
(You MUST wrap <Canvas> children that load assets in <Suspense> boundaries)
(You MUST reuse geometries and materials across meshes -- creating new instances per mesh wastes GPU memory)
(You MUST call event.stopPropagation() on pointer events to prevent hits passing through to occluded objects)
(You MUST use named constants for all numeric values -- positions, sizes, speeds, colors -- NO magic numbers)
</critical_requirements>
Auto-detection: React Three Fiber, R3F, @react-three/fiber, @react-three/drei, @react-three/rapier, @react-three/postprocessing, Canvas, useFrame, useThree, useLoader, useGLTF, mesh, boxGeometry, meshStandardMaterial, OrbitControls, drei, three.js, 3D scene, WebGL, instancedMesh
When to use:
When NOT to use:
Key patterns covered:
useFrame and refsuseLoader, useGLTF, and Suspense@react-three/rapier (RigidBody, colliders, collision events)@react-three/postprocessingDetailed Resources:
React Three Fiber is a React reconciler for Three.js -- every Three.js object becomes a declarative JSX element. The React tree IS the scene graph. Components mount/unmount meshes, lights, and cameras just like DOM elements. This means React features (Suspense, context, refs, state) all work naturally in 3D.
Core principles:
useRef in useFrame, structural changes (adding/removing objects) go through React state<Suspense> for automatic loading statesstopPropagation prevents hits on occluded objectsThe R3F ecosystem:
| Package | Purpose |
| ----------------------------- | --------------------------------------------------------------- |
| @react-three/fiber | Core renderer -- Canvas, hooks, reconciler |
| @react-three/drei | Helpers -- controls, loaders, abstractions, text, HTML overlays |
| @react-three/rapier | Physics -- rigid bodies, colliders, collision events |
| @react-three/postprocessing | Effects -- bloom, DOF, SSAO, vignette |
<Canvas> creates a WebGL context with scene, camera, and renderer. All R3F hooks must be used inside Canvas.
import { Canvas } from "@react-three/fiber";
const CAMERA_FOV = 50;
const CAMERA_POSITION: [number, number, number] = [0, 2, 5];
export function Scene() {
return (
<Canvas
camera={{
fov: CAMERA_FOV,
position: CAMERA_POSITION,
near: 0.1,
far: 100,
}}
shadows
dpr={[1, 2]}
frameloop="always"
>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} castShadow />
<mesh castShadow receiveShadow>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
</Canvas>
);
}
Why good: named position constant, shadows enabled on canvas + individual meshes, dpr clamped to prevent excessive resolution on HiDPI displays
See examples/core.md Pattern 1 for full Canvas config, lighting setups, and camera types.
useFrame runs every frame before render. Mutate refs directly -- never call setState.
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import type { Mesh } from "three";
const ROTATION_SPEED = 1;
export function SpinningBox() {
const meshRef = useRef<Mesh>(null);
useFrame((state, delta) => {
if (!meshRef.current) return;
meshRef.current.rotation.y += ROTATION_SPEED * delta;
});
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="royalblue" />
</mesh>
);
}
Why good: delta-time multiplication makes animation frame-rate independent, ref mutation avoids re-renders, guard clause prevents null access
// BAD: triggers re-render every frame -- destroys performance
function BadSpinningBox() {
const [rotation, setRotation] = useState(0);
useFrame((_, delta) => {
setRotation((r) => r + delta); // setState in useFrame!
});
return <mesh rotation-y={rotation} />;
}
Why bad: setState in useFrame causes a React re-render every frame (~60/s), defeating the purpose of direct GPU mutations
See examples/core.md Pattern 2 for animation with useFrame, clock-based motion, and conditional animation.
Use useLoader or drei's useGLTF to load models, textures, and other assets. Always wrap in <Suspense>.
import { Suspense } from "react";
import { useGLTF } from "@react-three/drei";
function Model({ url }: { url: string }) {
const { nodes, materials } = useGLTF(url);
return (
<mesh
geometry={(nodes.myMesh as THREE.Mesh).geometry}
material={materials.myMaterial}
/>
);
}
// Preload for faster initial render
useGLTF.preload("/model.glb");
export function SceneWithModel() {
return (
<Canvas>
<Suspense fallback={null}>
<Model url="/model.glb" />
</Suspense>
</Canvas>
);
}
Why good: Suspense handles loading states automatically, preload avoids waterfall, useGLTF extracts named nodes/materials
See examples/core.md Pattern 3 for texture loading, Draco compression, and progressive loading.
R3F meshes support DOM-like pointer events. Events are raycasted -- the nearest hit object receives the event first.
const HOVER_COLOR = "hotpink";
const DEFAULT_COLOR = "orange";
const ACTIVE_SCALE = 1.2;
const DEFAULT_SCALE = 1;
export function InteractiveBox() {
const [hovered, setHovered] = useState(false);
const [active, setActive] = useState(false);
return (
<mesh
scale={active ? ACTIVE_SCALE : DEFAULT_SCALE}
onClick={(e) => {
e.stopPropagation();
setActive((a) => !a);
}}
onPointerOver={(e) => {
e.stopPropagation();
setHovered(true);
}}
onPointerOut={() => setHovered(false)}
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? HOVER_COLOR : DEFAULT_COLOR} />
</mesh>
);
}
Why good: stopPropagation prevents clicks passing through to objects behind, hover state uses React state (not per-frame), named color constants
See examples/interaction.md for event object properties, pointer capture, drag, and onPointerMissed.
@react-three/drei provides ready-made abstractions for common tasks.
import { OrbitControls, Environment, Text, Html } from "@react-three/drei";
// Camera controls
<OrbitControls enableDamping dampingFactor={0.1} />
// Environment lighting from HDRI preset
<Environment preset="sunset" background />
// 3D text rendered as mesh geometry
<Text fontSize={0.5} position={[0, 2, 0]} color="white">
Hello 3D World
</Text>
// HTML overlaid on 3D position
<Html position={[1, 1, 0]} distanceFactor={10}>
<div className="tooltip">Click me</div>
</Html>
See examples/core.md Pattern 5 for full drei helper examples including Detailed (LOD), ContactShadows, and Float.
Wrap the scene in <Physics> and objects in <RigidBody> for physics simulation.
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
const GRAVITY: [number, number, number] = [0, -9.81, 0];
const FLOOR_SIZE: [number, number, number] = [10, 0.1, 10];
export function PhysicsScene() {
return (
<Physics gravity={GRAVITY}>
{/* Dynamic falling box */}
<RigidBody colliders="cuboid" restitution={0.5}>
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="tomato" />
</mesh>
</RigidBody>
{/* Static floor */}
<RigidBody type="fixed">
<mesh>
<boxGeometry args={FLOOR_SIZE} />
<meshStandardMaterial color="gray" />
</mesh>
</RigidBody>
</Physics>
);
}
Why good: gravity as named constant, explicit collider type, fixed body for static geometry
See examples/core.md Pattern 6 for collision events, sensors, and InstancedRigidBodies.
@react-three/postprocessing merges effects into efficient render passes.
import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";
const BLOOM_INTENSITY = 0.5;
const BLOOM_LUMINANCE_THRESHOLD = 0.9;
const VIGNETTE_DARKNESS = 0.5;
<EffectComposer>
<Bloom
intensity={BLOOM_INTENSITY}
luminanceThreshold={BLOOM_LUMINANCE_THRESHOLD}
/>
<Vignette darkness={VIGNETTE_DARKNESS} />
</EffectComposer>;
See reference.md for common effect combinations and performance considerations.
Use <instancedMesh> to render thousands of identical objects in a single draw call.
import { useRef, useEffect, useMemo } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
const INSTANCE_COUNT = 1000;
export function Particles() {
const meshRef = useRef<THREE.InstancedMesh>(null);
const dummy = useMemo(() => new THREE.Object3D(), []);
useEffect(() => {
if (!meshRef.current) return;
for (let i = 0; i < INSTANCE_COUNT; i++) {
dummy.position.set(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
);
dummy.updateMatrix();
meshRef.current.setMatrixAt(i, dummy.matrix);
}
meshRef.current.instanceMatrix.needsUpdate = true;
}, [dummy]);
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, INSTANCE_COUNT]}>
<sphereGeometry args={[0.05, 8, 8]} />
<meshBasicMaterial color="white" />
</instancedMesh>
);
}
Why good: single draw call for 1000 objects, useMemo prevents recreating dummy each render, named count constant
See examples/performance.md for animated instances, LOD with Detailed, and on-demand rendering.
Create shared resources and reference them across meshes to reduce GPU overhead.
import * as THREE from "three";
import { useMemo } from "react";
export function SharedGeometryScene() {
const sharedGeo = useMemo(() => new THREE.SphereGeometry(0.5, 32, 32), []);
const sharedMat = useMemo(
() => new THREE.MeshStandardMaterial({ color: "coral" }),
[],
);
return (
<>
<mesh geometry={sharedGeo} material={sharedMat} position={[-2, 0, 0]} />
<mesh geometry={sharedGeo} material={sharedMat} position={[0, 0, 0]} />
<mesh geometry={sharedGeo} material={sharedMat} position={[2, 0, 0]} />
</>
);
}
Why good: one geometry + one material in GPU memory regardless of mesh count, useMemo prevents recreation on re-render
See examples/performance.md for disposal patterns and PerformanceMonitor.
</patterns><decision_framework>
Is it a per-frame continuous animation (rotation, bob, orbit)?
├─ YES → useFrame + useRef (mutate directly, never setState)
└─ NO → Is it triggered by user interaction (hover, click)?
├─ YES → React state for discrete changes (scale, color)
│ or spring-based animation libraries for smooth transitions
└─ NO → Is it a one-time entrance animation?
└─ YES → useFrame with a progress ref that clamps at 1.0
Is the shape a box?
├─ YES → "cuboid" (fastest)
└─ NO → Is it a sphere?
├─ YES → "ball" (fast)
└─ NO → Is it convex (no holes/concavities)?
├─ YES → "hull" (good balance)
└─ NO → "trimesh" (expensive, use sparingly)
Are there 10+ identical objects?
├─ YES → instancedMesh (single draw call)
└─ NO → Are objects at varying distances?
├─ YES → LOD with drei's Detailed component
└─ NO → Is the scene mostly static?
├─ YES → frameloop="demand" + invalidate()
└─ NO → Check draw calls (target < 200)
</decision_framework>
<red_flags>
High Priority Issues:
setState inside useFrame -- causes 60 re-renders/second, destroys performanceVector3/Matrix4/Object3D instances inside useFrame -- allocates memory every frame, triggers GC pauses<Suspense> around components using useLoader/useGLTF -- causes uncaught promise errorsuseMemo or module-level instancesevent.stopPropagation() on pointer events -- clicks pass through to occluded objects unexpectedlyMedium Priority Issues:
frameloop="always" for mostly-static scenes -- drains battery; use "demand" with invalidate()<mesh> is a draw call) -- use instancing or merge geometriesGotchas & Edge Cases:
useFrame, useThree, useLoader) must be called inside <Canvas> -- they depend on fiber contextuseFrame callbacks with renderPriority >= 1 take over the render loop -- you must call gl.render() manuallyuseThree selectors for Three.js internal properties (like camera.zoom) are NOT reactive -- use invalidate() after imperative changesposition={[x, y, z]} where Y is vertical<instancedMesh args={[null, null, count]}> -- the first two args (geometry, material) should be undefined when using child elementsonPointerMissed fires on the Canvas element for clicks that hit no mesh -- useful for deselectionnew THREE.*() calls need manual .dispose()<Physics> from rapier must be wrapped in <Suspense> because it loads WASM asynchronouslydelta property is mouse-down-to-mouse-up distance in pixels -- useful for distinguishing clicks from drags</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST never call setState inside useFrame -- mutate refs directly for per-frame updates)
(You MUST wrap <Canvas> children that load assets in <Suspense> boundaries)
(You MUST reuse geometries and materials across meshes -- creating new instances per mesh wastes GPU memory)
(You MUST call event.stopPropagation() on pointer events to prevent hits passing through to occluded objects)
(You MUST use named constants for all numeric values -- positions, sizes, speeds, colors -- NO magic numbers)
Failure to follow these rules will cause frame drops, memory leaks, broken interactions, and poor 3D performance.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events