React Three Fiber renders Three.js scenes declaratively as React components — <Canvas> creates the WebGL context, <mesh> wraps geometry and material, and useFrame drives the animation loop. All Three.js constructors map to lowercase JSX: <boxGeometry args={[1, 1, 1]} />, <meshStandardMaterial color="hotpink" />. useThree() accesses the renderer, camera, scene, and size. The @react-three/drei package adds helpers: OrbitControls, Environment, Text, Html, useGLTF, useTexture, Stars, and Sparkles. useGLTF(url) loads GLB models with auto-generated TypeScript types from gltfjsx. @react-three/rapier adds physics with RigidBody and Collider. useRef<THREE.Mesh>() targets mesh nodes for imperative updates. Raycasting through onClick, onPointerOver, and onPointerOut events on meshes enables interaction. Claude Code generates React Three Fiber scene setups, animated meshes, model loading, physics simulations, and interactive 3D interfaces.
CLAUDE.md for React Three Fiber
## React Three Fiber Stack
- Version: @react-three/fiber >= 8.15, @react-three/drei >= 9.100, three >= 0.165
- Canvas: <Canvas camera={{ position: [0, 2, 5], fov: 60 }} shadows> — scene root
- Mesh: <mesh castShadow><boxGeometry args={[1,1,1]}/><meshStandardMaterial/></mesh>
- Animation: useFrame((state, delta) => { ref.current.rotation.y += delta }) — 60fps loop
- Camera: const { camera, gl, scene, size } = useThree() — renderer access
- Models: const { nodes, materials } = useGLTF("/model.glb") — drei hook
- Physics: <Physics><RigidBody><mesh/></RigidBody></Physics> — rapier integration
- Lighting: <ambientLight/> + <directionalLight castShadow/> — standard PBR rig
Basic Scene Setup
// components/3d/ProductShowcase.tsx — interactive product viewer
"use client"
import { Canvas, useFrame, useThree } from "@react-three/fiber"
import {
OrbitControls,
Environment,
ContactShadows,
Html,
PresentationControls,
} from "@react-three/drei"
import { useRef, useState, Suspense } from "react"
import * as THREE from "three"
function Box({
color = "#3b82f6",
onClick,
}: {
color?: string
onClick?: () => void
}) {
const meshRef = useRef<THREE.Mesh>(null!)
const [hovered, setHovered] = useState(false)
const [active, setActive] = useState(false)
useFrame((state, delta) => {
meshRef.current.rotation.x += delta * 0.3
meshRef.current.rotation.y += delta * 0.5
// Pulse scale on hover
const targetScale = hovered ? 1.2 : 1
meshRef.current.scale.setScalar(
THREE.MathUtils.lerp(meshRef.current.scale.x, targetScale, 0.1)
)
})
return (
<mesh
ref={meshRef}
onClick={() => {
setActive(!active)
onClick?.()
}}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
castShadow
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial
color={hovered ? "#f97316" : color}
metalness={0.5}
roughness={0.3}
emissive={active ? color : "#000000"}
emissiveIntensity={active ? 0.3 : 0}
/>
</mesh>
)
}
function FloatingLabel({ text, position }: { text: string; position: [number, number, number] }) {
return (
<Html position={position} center distanceFactor={6}>
<div className="bg-black/80 text-white text-xs px-2 py-1 rounded whitespace-nowrap pointer-events-none">
{text}
</div>
</Html>
)
}
export function ProductShowcase() {
return (
<div className="w-full h-96 rounded-xl overflow-hidden border">
<Canvas
shadows
camera={{ position: [0, 2, 5], fov: 50 }}
gl={{ antialias: true, alpha: true }}
>
<color attach="background" args={["#f8fafc"]} />
{/* Lighting */}
<ambientLight intensity={0.5} />
<directionalLight
position={[5, 5, 5]}
intensity={1}
castShadow
shadow-mapSize={[2048, 2048]}
/>
<pointLight position={[-5, 5, -5]} intensity={0.5} color="#6366f1" />
<Suspense fallback={null}>
<PresentationControls
global
rotation={[0.13, 0.1, 0]}
polar={[-0.4, 0.2]}
azimuth={[-1, 0.75]}
config={{ mass: 2, tension: 400 }}
snap={{ mass: 4, tension: 400 }}
>
<Box />
<FloatingLabel text="Click to interact" position={[0, 1.5, 0]} />
</PresentationControls>
<Environment preset="city" />
<ContactShadows
position={[0, -1.5, 0]}
opacity={0.4}
scale={5}
blur={2}
/>
</Suspense>
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2} />
</Canvas>
</div>
)
}
GLTF Model Loading
// components/3d/ProductModel.tsx — load GLB/GLTF models
import { useGLTF, useAnimations } from "@react-three/drei"
import { useEffect, useRef } from "react"
import { useFrame } from "@react-three/fiber"
import * as THREE from "three"
// gltfjsx generates this type — run: npx gltfjsx model.glb --types
interface ProductGLTF {
nodes: {
Body: THREE.Mesh
Screen: THREE.Mesh
Buttons: THREE.Mesh
}
materials: {
Aluminum: THREE.MeshStandardMaterial
Glass: THREE.MeshPhysicalMaterial
}
animations: THREE.AnimationClip[]
}
// Preload for faster first render
useGLTF.preload("/models/product.glb")
export function ProductModel({
position = [0, 0, 0] as [number, number, number],
}: {
position?: [number, number, number]
}) {
const group = useRef<THREE.Group>(null!)
const { nodes, materials, animations } = useGLTF("/models/product.glb") as unknown as ProductGLTF
const { actions } = useAnimations(animations, group)
// Play idle animation on mount
useEffect(() => {
actions["Idle"]?.play()
}, [actions])
// Float animation
useFrame(state => {
group.current.position.y = Math.sin(state.clock.elapsedTime * 0.5) * 0.1
group.current.rotation.y = state.clock.elapsedTime * 0.2
})
return (
<group ref={group} position={position} dispose={null}>
<mesh
castShadow
receiveShadow
geometry={nodes.Body.geometry}
material={materials.Aluminum}
/>
<mesh
castShadow
geometry={nodes.Screen.geometry}
material={materials.Glass}
/>
<mesh
geometry={nodes.Buttons.geometry}
material={materials.Aluminum}
/>
</group>
)
}
Particle System
// components/3d/ParticleField.tsx — instanced mesh for performance
import { useRef, useMemo } from "react"
import { useFrame } from "@react-three/fiber"
import * as THREE from "three"
const COUNT = 2000
export function ParticleField() {
const meshRef = useRef<THREE.InstancedMesh>(null!)
// Generate initial positions
const { positions, speeds } = useMemo(() => {
const positions = Float32Array.from({ length: COUNT * 3 }, () => (Math.random() - 0.5) * 20)
const speeds = Float32Array.from({ length: COUNT }, () => Math.random() * 0.02 + 0.005)
return { positions, speeds }
}, [])
const dummy = useMemo(() => new THREE.Object3D(), [])
const clock = useRef(0)
// Initialize instance transforms
useFrame((state, delta) => {
clock.current += delta
for (let i = 0; i < COUNT; i++) {
const xi = i * 3
dummy.position.set(
positions[xi]! + Math.sin(clock.current * speeds[i]! + i) * 0.1,
positions[xi + 1]! + Math.sin(clock.current * 0.5 + i) * 0.05,
positions[xi + 2]!
)
dummy.scale.setScalar(Math.sin(clock.current + i) * 0.3 + 0.7)
dummy.updateMatrix()
meshRef.current.setMatrixAt(i, dummy.matrix)
}
meshRef.current.instanceMatrix.needsUpdate = true
})
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
<sphereGeometry args={[0.02, 4, 4]} />
<meshBasicMaterial color="#6366f1" transparent opacity={0.6} />
</instancedMesh>
)
}
Physics with Rapier
// components/3d/PhysicsScene.tsx — @react-three/rapier
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier"
import { Canvas } from "@react-three/fiber"
import { OrbitControls } from "@react-three/drei"
import { useRef } from "react"
import * as THREE from "three"
function PhysicsBox({ position }: { position: [number, number, number] }) {
return (
<RigidBody position={position} restitution={0.7} friction={0.5}>
<mesh castShadow>
<boxGeometry args={[0.5, 0.5, 0.5]} />
<meshStandardMaterial color={`hsl(${Math.random() * 360}, 70%, 60%)`} />
</mesh>
</RigidBody>
)
}
export function PhysicsDemo() {
return (
<Canvas camera={{ position: [0, 5, 10], fov: 50 }} shadows>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 5]} castShadow intensity={1} />
<Physics gravity={[0, -9.81, 0]}>
{/* Floor */}
<RigidBody type="fixed">
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]} position={[0, -1, 0]}>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial color="#e2e8f0" />
</mesh>
<CuboidCollider args={[10, 0.1, 10]} position={[0, -1, 0]} />
</RigidBody>
{/* Falling boxes */}
{Array.from({ length: 20 }, (_, i) => (
<PhysicsBox
key={i}
position={[
(Math.random() - 0.5) * 4,
Math.random() * 8 + 2,
(Math.random() - 0.5) * 4,
]}
/>
))}
</Physics>
<OrbitControls />
</Canvas>
)
}
For the Babylon.js alternative when more built-in game-engine features are needed — Babylon includes a full inspector GUI, PBR material library, physics integration, GUI system, and animation editor in the core package without needing React integration layers, see the WebGL engine comparison for game-focused use cases. For the Spline alternative when design-tool integration is the priority — Spline.design exports scenes directly to embeddable React components with no Three.js knowledge required, useful for marketing page animations created by designers, see the 3D web design tools guide. The Claude Skills 360 bundle includes React Three Fiber skill sets covering scenes, animations, model loading, and physics. Start with the free tier to try 3D web generation.