EmpireUI
Get Pro
← Blog9 min read#react-three-fiber#3d#three.js

react-three-fiber Intro: 3D Scenes in React With Three.js

Learn how react-three-fiber bridges Three.js and React so you can build real 3D scenes with JSX, hooks, and familiar component patterns — no boilerplate required.

abstract 3D rendered geometric shapes glowing on dark background

Why react-three-fiber Exists

Vanilla Three.js is powerful, but it fights React the whole way. You're manually querying the DOM, storing scene references in refs, running your own animation loop, and cleaning up after yourself on unmount. It's tedious, error-prone, and doesn't compose well with the rest of your component tree.

react-three-fiber (r3f) — released in 2019 and now on v8 — wraps Three.js in a React renderer. Every Three.js object becomes a JSX element. <mesh>, <boxGeometry>, <pointLight>. The reconciler manages the scene graph just like React manages the DOM, so your 3D scene and your UI components share the same mental model.

In practice, the payoff is huge. Hooks work the way you expect. State drives geometry. You get hot module replacement for free. And you can drop in any r3f-compatible ecosystem package — @react-three/drei, @react-three/postprocessing, @react-three/rapier — without gluing things together yourself.

Look, if you already know React, you already know 80% of r3f. The other 20% is Three.js fundamentals: scenes, cameras, meshes, materials, lights. Let's cover all of it.

Installing and Setting Up Your First Canvas

Start clean. You need three packages: Three.js itself, r3f, and optionally @react-three/drei for the helper abstractions you'll use constantly.

npm install three @react-three/fiber @react-three/drei

The entry point is <Canvas>. Drop it anywhere in your JSX and it creates a WebGL context, a default scene, a perspective camera, and a requestAnimationFrame render loop — all managed for you.

import { Canvas } from '@react-three/fiber'

export default function App() {
  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <Canvas>
        {/* your 3D scene goes here */}
      </Canvas>
    </div>
  )
}

Worth noting: <Canvas> fills its parent container, so you control the size via CSS on the wrapper div. Set height: 100vh for a full-screen scene or constrain it to a card. Either way, the canvas stays responsive without any resize observers on your end.

Meshes, Geometry, and Materials

Everything you see in a Three.js scene is a mesh — a geometry (the shape) paired with a material (how it looks). In r3f, you write them as nested JSX children using lowercase tags that map 1:1 to Three.js constructors.

import { Canvas } from '@react-three/fiber'

function Box() {
  return (
    <mesh>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color="hotpink" />
    </mesh>
  )
}

export default function App() {
  return (
    <Canvas>
      <ambientLight intensity={0.5} />
      <pointLight position={[10, 10, 10]} />
      <Box />
    </Canvas>
  )
}

The args prop maps to the constructor arguments. <boxGeometry args={[1, 1, 1]} /> is equivalent to new THREE.BoxGeometry(1, 1, 1). This pattern applies to every Three.js class — geometry, material, texture, whatever. Once that clicks, you can translate any Three.js example to r3f by inspection.

Quick aside: meshStandardMaterial uses physically-based rendering (PBR), so it responds correctly to lights. Swap in meshBasicMaterial if you want a flat unlit color — useful for debugging, less useful for anything that needs to look good.

Positioning, rotation, and scale all go on the <mesh> element as props: position={[x, y, z]}, rotation={[x, y, z]}, scale={[x, y, z]}. Or you can pass a single number to scale and it applies uniformly. Coordinates are in Three.js units — by default, 1 unit is roughly 1 meter when using the default perspective camera.

Animation With useFrame

r3f exposes a useFrame hook that runs your callback on every rendered frame, after the scene is updated and before it's drawn. It receives the r3f state object and the elapsed time delta. This is where all your animation logic lives.

import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import type { Mesh } from 'three'

function SpinningBox() {
  const meshRef = useRef<Mesh>(null)

  useFrame((state, delta) => {
    if (!meshRef.current) return
    meshRef.current.rotation.x += delta
    meshRef.current.rotation.y += delta * 0.5
  })

  return (
    <mesh ref={meshRef}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color="#7c3aed" />
    </mesh>
  )
}

Using delta instead of a fixed increment means your animation runs at the same speed regardless of frame rate. On a 144Hz display or a slow 30fps mobile GPU, the box rotates identically. Always use delta.

Honestly, useFrame is where most people get confused at first. It's not a React state update — it's an imperative mutation of the Three.js object. You're reaching into meshRef.current and changing properties directly. React's reconciler doesn't re-render on every frame. That's intentional — it'd be catastrophically slow if it did.

One more thing — useFrame only works inside the <Canvas> subtree. Call it from any component that's a descendant of <Canvas> and it works. Call it outside and you'll get an error about missing context.

Lights, Cameras, and Environment

Three.js has several light types: ambientLight (fills everything uniformly), pointLight (radiates outward from a point, like a bulb), directionalLight (parallel rays, like sunlight), and spotLight (a cone of light). For most scenes you'll want at least an ambient light and one directional or point light.

<Canvas camera={{ position: [0, 0, 5], fov: 60 }}>
  <ambientLight intensity={0.3} />
  <directionalLight position={[5, 5, 5]} intensity={1.5} castShadow />
  <SpinningBox />
</Canvas>

The camera prop on <Canvas> lets you override the default camera. The default FOV is 75 degrees — which is fine for most things, but 60 feels more natural for architectural or product visualization work. Camera position [0, 0, 5] puts you 5 units back from the origin, looking forward.

If you want a proper environment map for reflections and ambient lighting, @react-three/drei has an <Environment> component that loads HDR presets with one line. Swap in a preset like 'studio', 'sunset', or 'city' and you get convincing reflections on metallic or glossy materials instantly. It's the fastest path from gray clay to something that looks production-ready.

import { Environment, OrbitControls } from '@react-three/drei'

// Inside <Canvas>:
<Environment preset="studio" />
<OrbitControls />

That <OrbitControls> component from drei is also worth adding during development. Click-drag to orbit, scroll to zoom, right-click to pan. You get interactive camera control without writing a single event handler. Remove it for production or restrict it if you want a fixed camera angle.

Loading 3D Models and Textures

Most real projects need external assets — 3D models in glTF/GLB format and image textures. r3f handles both through hooks from @react-three/drei. The useGLTF hook loads a glTF file and returns the scene graph, cameras, and animations. The useTexture hook loads images and returns a Three.js texture object.

import { useGLTF } from '@react-three/drei'

function Model({ url }: { url: string }) {
  const { scene } = useGLTF(url)
  return <primitive object={scene} />
}

// Preload so the model is in cache before the component mounts
useGLTF.preload('/models/helmet.glb')

Put your .glb files in /public and reference them with a root-relative path. Next.js serves static assets from there with no configuration. One thing that trips people up: the model file path in useGLTF is relative to the running server, not the source file. /models/helmet.glb always works; ../../models/helmet.glb doesn't.

For textures on custom geometry, useTexture is the right hook. It accepts a single path or an object with named keys (map, normalMap, roughnessMap, etc.) and returns the corresponding texture objects ready to drop onto a material.

import { useTexture } from '@react-three/drei'

function TexturedPlane() {
  const { map, normalMap } = useTexture({
    map: '/textures/wood-color.jpg',
    normalMap: '/textures/wood-normal.jpg',
  })

  return (
    <mesh rotation={[-Math.PI / 2, 0, 0]}>
      <planeGeometry args={[4, 4]} />
      <meshStandardMaterial map={map} normalMap={normalMap} />
    </mesh>
  )
}

Mixing r3f With Your UI Layer

Here's something that surprises a lot of devs: r3f scenes don't have to be isolated full-screen experiences. You can layer HTML and CSS over a canvas with @react-three/drei's <Html> component, which positions DOM elements in 3D space and keeps them in sync as the camera moves.

import { Html } from '@react-three/drei'

function AnnotatedMesh() {
  return (
    <mesh position={[0, 0, 0]}>
      <sphereGeometry args={[1, 32, 32]} />
      <meshStandardMaterial color="cyan" />
      <Html position={[1.2, 0.5, 0]} distanceFactor={10}>
        <div className="bg-white/10 backdrop-blur rounded px-2 py-1 text-sm">
          Interactive label
        </div>
      </Html>
    </mesh>
  )
}

That label div is real HTML — you can use Tailwind classes, glassmorphism effects, click handlers, anything. If you're building something like a 3D product configurator or an interactive diagram, this is how you add UI without fighting the canvas boundary. Check out the glassmorphism components on Empire UI for ready-made panels that work well over 3D canvases, or use the glassmorphism generator to dial in the blur and opacity before writing any CSS.

That said, keep your render work inside useFrame and your UI state in normal React state. Don't try to drive Three.js from React state updates on every frame — that triggers reconciler work at 60fps and tanks performance fast. The rule of thumb: slow-changing state (camera mode, selected object, color theme) lives in React state. Per-frame animation mutations happen imperatively inside useFrame.

For more advanced visual effects on top of your 3D scenes — think aurora-style glow, gradient overlays, or UI components that blend with your WebGL output — you can pull from the aurora or gradient generator tools to get CSS that composites naturally over a transparent canvas. Set gl={{ alpha: true }} on <Canvas> and the background renders transparent so your CSS background shows through.

FAQ

Do I need to know Three.js before learning react-three-fiber?

Some Three.js fundamentals help — meshes, geometry, materials, lights — but you don't need to be an expert. The r3f docs walk you through the Three.js concepts as you need them, and the JSX API makes most things discoverable.

Is react-three-fiber production-ready?

Yes. It's been in production since 2019, powers commercial products and games, and v8 is stable. The ecosystem around it (drei, postprocessing, rapier) is mature enough that you're unlikely to hit gaps for typical use cases.

How does r3f affect bundle size?

Three.js itself is around 600 KB minified before tree-shaking. r3f adds roughly 30 KB on top. Use dynamic imports to code-split your 3D scene so it doesn't block your initial page load.

Can I use react-three-fiber with Next.js?

Yes, but you need to disable SSR for your canvas component — Three.js requires the browser's WebGL context, which doesn't exist on the server. Wrap it with dynamic(() => import('./Scene'), { ssr: false }).

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

React Three Fiber: 3D Graphics in React Without WebGL ExpertiseReact Hooks in 2026: A Complete Guide with Real-World ExamplesThree.js with React: Particles, Blobs and Interactive 3D ScenesReact Three Fiber in 2026: 3D Scenes, Shaders and Performance