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.

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>