React Three.js: 3D Graphics with React Three Fiber (2026)
React Three Fiber (R3F) is the React renderer for Three.js — every Three.js object becomes a JSX element, state drives geometry and materials, and hooks like useFrame power the animation loop. Combined with the Drei helper library, you get camera controls, GLTF loaders, environment maps and post-processing in a few lines. This guide goes from a first spinning cube to loading 3D models and adding bloom effects.
Table of Contents
Setup
npm install three @react-three/fiber @react-three/drei
npm install -D @types/three
// Vite note: add to vite.config.ts if you see issues with three imports
// optimizeDeps: { include: ['three'] }
First Scene
import { Canvas } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
// Canvas sets up the WebGL renderer, scene and camera
function App() {
return (
<div style={{ width: '100%', height: 500 }}>
<Canvas
camera={{ position: [0, 0, 5], fov: 75 }}
shadows
gl={{ antialias: true, alpha: true }}
>
{/* Ambient + directional light */}
<ambientLight intensity={0.4} />
<directionalLight position={[10, 10, 5]} intensity={1.5} castShadow />
<SpinningCube />
{/* Mouse/touch orbit controls */}
<OrbitControls enablePan={false} maxDistance={10} minDistance={2} />
</Canvas>
</div>
)
}
// R3F turns Three.js class names into lowercase JSX elements:
// new THREE.Mesh() → <mesh>
// new THREE.BoxGeometry() → <boxGeometry>
// new THREE.MeshStandardMaterial() → <meshStandardMaterial>
function SpinningCube() {
return (
<mesh rotation={[0.4, 0.4, 0]} castShadow>
<boxGeometry args={[2, 2, 2]} /> {/* width, height, depth */}
<meshStandardMaterial color="#6366f1" metalness={0.3} roughness={0.4} />
</mesh>
)
}
Animation with useFrame
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import type { Mesh } from 'three'
function AnimatedSphere() {
const meshRef = useRef<Mesh>(null)
const [hovered, setHovered] = useState(false)
const [clicked, setClicked] = useState(false)
// useFrame runs every animation frame (60fps)
// state.clock.elapsedTime is the time since mount in seconds
useFrame((state, delta) => {
if (!meshRef.current) return
// delta = time since last frame (use for frame-rate independent animation)
meshRef.current.rotation.y += delta * 0.5
meshRef.current.rotation.x = Math.sin(state.clock.elapsedTime * 0.5) * 0.3
// Oscillate position
meshRef.current.position.y = Math.sin(state.clock.elapsedTime) * 0.5
})
return (
<mesh
ref={meshRef}
scale={clicked ? 1.5 : hovered ? 1.2 : 1}
onClick={() => setClicked(c => !c)}
onPointerOver={() => { setHovered(true); document.body.style.cursor = 'pointer' }}
onPointerOut={() => { setHovered(false); document.body.style.cursor = 'auto' }}
>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardMaterial
color={hovered ? '#22d3ee' : '#6366f1'}
wireframe={clicked}
/>
</mesh>
)
}
Lighting and Materials
import { Environment, useTexture } from '@react-three/drei'
function LitScene() {
const [colorMap, normalMap, roughnessMap] = useTexture([
'/textures/stone-color.jpg',
'/textures/stone-normal.jpg',
'/textures/stone-roughness.jpg',
])
return (
<>
{/* Environment map from Drei — provides realistic reflections */}
<Environment preset="city" /> {/* city | dawn | forest | sunset | night | ... */}
{/* Point lights */}
<pointLight position={[-5, 5, 5]} intensity={50} color="#6366f1" />
<pointLight position={[5, -5, 5]} intensity={30} color="#22d3ee" />
<mesh>
<sphereGeometry args={[1.5, 128, 128]} />
<meshStandardMaterial
map={colorMap}
normalMap={normalMap}
roughnessMap={roughnessMap}
metalness={0.1}
envMapIntensity={0.8}
/>
</mesh>
{/* Ground plane with shadows */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -2, 0]} receiveShadow>
<planeGeometry args={[20, 20]} />
<shadowMaterial opacity={0.3} />
</mesh>
</>
)
}
Camera Controls with Drei
import {
OrbitControls,
PerspectiveCamera,
CameraShake,
FlyControls,
FirstPersonControls,
} from '@react-three/drei'
// OrbitControls — mouse drag to orbit, scroll to zoom
<OrbitControls
enablePan={true}
enableZoom={true}
enableRotate={true}
autoRotate={true}
autoRotateSpeed={2}
minPolarAngle={Math.PI / 6} // Don't go below 30°
maxPolarAngle={Math.PI / 2} // Don't go above 90°
maxDistance={20}
minDistance={2}
/>
// Programmatic camera animation
import { useThree } from '@react-three/fiber'
import { gsap } from 'gsap'
function CameraAnimator({ target }: { target: [number, number, number] }) {
const { camera } = useThree()
useEffect(() => {
gsap.to(camera.position, {
x: target[0], y: target[1], z: target[2],
duration: 1.5, ease: 'power2.inOut',
})
}, [target])
return null
}
Loading GLTF Models
import { useGLTF, useAnimations } from '@react-three/drei'
// Auto-generate typed hook: npx gltfjsx model.glb -t
function CharacterModel({ url }: { url: string }) {
const { scene, animations } = useGLTF(url)
const { ref, actions, names } = useAnimations(animations)
useEffect(() => {
// Play first animation on mount
actions[names[0]]?.play()
}, [actions, names])
return <primitive ref={ref} object={scene} scale={0.01} />
}
// Preload to avoid pop-in
useGLTF.preload('/models/character.glb')
// DRACOLoader for compressed models (significantly smaller files)
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
// R3F handles this automatically when you configure:
// <Canvas><Suspense><CharacterModel url="/model.glb" /></Suspense></Canvas>
// Suspense for loading state
function Scene() {
return (
<Suspense fallback={<LoadingBox />}>
<CharacterModel url="/models/character.glb" />
</Suspense>
)
}
function LoadingBox() {
const meshRef = useRef<Mesh>(null)
useFrame(() => { if (meshRef.current) meshRef.current.rotation.y += 0.02 })
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="#6366f1" wireframe />
</mesh>
)
}
Post-Processing Effects
npm install @react-three/postprocessing postprocessing
import { EffectComposer, Bloom, ChromaticAberration, Vignette } from '@react-three/postprocessing'
import { BlendFunction } from 'postprocessing'
function PostProcessedScene() {
return (
<Canvas>
<Scene />
<EffectComposer>
{/* Bloom — glow around bright areas */}
<Bloom
intensity={1.5}
luminanceThreshold={0.8}
luminanceSmoothing={0.9}
mipmapBlur
/>
{/* Chromatic aberration — subtle RGB shift */}
<ChromaticAberration
blendFunction={BlendFunction.NORMAL}
offset={[0.002, 0.002]}
/>
{/* Vignette — darken edges */}
<Vignette eskil={false} offset={0.1} darkness={1.1} />
</EffectComposer>
</Canvas>
)
}
Performance Tips
import { Instances, Instance, BakeShadows, AdaptiveDpr } from '@react-three/drei'
import { Perf } from 'r3f-perf'
// Instanced rendering — thousands of identical objects at GPU cost of one
function ParticleField({ count = 5000 }) {
const positions = useMemo(() =>
Array.from({ length: count }, () => [
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40,
]), [count])
return (
<Instances limit={count}>
<sphereGeometry args={[0.05, 8, 8]} />
<meshStandardMaterial color="#6366f1" />
{positions.map((pos, i) => (
<Instance key={i} position={pos as [number, number, number]} />
))}
</Instances>
)
}
// AdaptiveDpr — lower resolution on slow devices
<Canvas>
<AdaptiveDpr pixelated /> {/* Reduces DPR when FPS drops */}
<Perf position="top-left" /> {/* FPS counter for development */}
<BakeShadows /> {/* Freeze shadow maps — huge perf win for static scenes */}
</Canvas>