0306090120150180210240270300330km/h
0 km/h
ADIT JANGID

Creating Stunning 3D Web Experiences with Three.js and React Three Fiber

A deep dive tutorial into rendering interactive 3D web elements inside React applications using Three.js, React Three Fiber (R3F), and Drei.

Creating Stunning 3D Web Experiences with Three.js and React Three Fiber

3D graphics are no longer restricted to standalone desktop applications or gaming consoles. Thanks to WebGL and modern browser engines, we can render complex, interactive 3D scenes directly in the browser.

While raw Three.js is highly powerful, managing scene graphs, animation updates, resize listeners, and state syncing in a declarative framework like React can quickly become verbose. That is where React Three Fiber (R3F) comes in. Created by the Poimandres (pmndrs) developer collective, R3F is a React renderer for Three.js. It allows you to declare 3D objects as reusable React components while maintaining the exact performance of Three.js.

In this tutorial, we will build an interactive 3D scene complete with custom geometries, lighting, shadows, physics-like hover reactions, and orbital camera controls.


React Three Fiber Architecture

React Three Fiber translates React components directly into Three.js class instances:

graph TD
    Canvas[Canvas Component] --> Scene[three.js Scene]
    Canvas --> Camera[three.js Camera]
    Canvas --> Renderer[three.js WebGLRenderer]
    Scene --> AmbientLight[<ambientLight />]
    Scene --> DirectionalLight[<directionalLight />]
    Scene --> Mesh[<mesh />]
    Mesh --> Box[<boxGeometry />]
    Mesh --> Material[<meshStandardMaterial />]
  • Canvas: Handles resizing, rendering loop updates, and sets up a standard three.js Scene, Camera, and WebGLRenderer.
  • Mesh: Replaces new THREE.Mesh().
  • Geometries & Materials: Nested inside <mesh> elements, they tell Three.js the shape and texture of the objects.

Prerequisites & Dependencies

To get started, spin up a clean React or Next.js app, then install Three.js, React Three Fiber, and Drei (a library containing useful helpers like controls, loader animations, and preset materials):

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

Step 1: Writing the Canvas Wrapper

The <Canvas> component is the root container of any R3F application. It fills its parent container, so make sure the HTML element wrapping it has a fixed height and width (e.g. w-full h-96 or h-screen).

Create a component named app/components/ThreeScene.tsx:

"use client";

import { Canvas } from "@react-three/fiber";
import { OrbitControls, Stars } from "@react-three/drei";
import SpinningCube from "./SpinningCube";

export default function ThreeScene() {
  return (
    <div className="w-full h-screen bg-slate-950 flex flex-col justify-between">
      {/* Absolute Header Overlay */}
      <div className="absolute top-6 left-6 z-10 pointer-events-none">
        <h1 className="text-2xl font-black text-white uppercase tracking-wider">HyperCube</h1>
        <p className="text-xs text-teal-400 font-mono">React Three Fiber + Drei Demo</p>
      </div>

      <div className="absolute bottom-6 right-6 z-10 max-w-xs text-right pointer-events-none">
        <p className="text-xs text-slate-400">
          Left-click & drag to orbit. Right-click & drag to pan. Scroll to zoom in and out.
        </p>
      </div>

      {/* R3F Canvas Container */}
      <Canvas
        shadows
        camera={{ position: [3, 3, 5], fov: 45 }}
        className="w-full h-full"
      >
        {/* Adds stars in the background scene */}
        <Stars radius={100} depth={50} count={5000} factor={4} saturation={0} fade speed={1} />

        {/* Ambient light shines on all objects equally */}
        <ambientLight intensity={0.2} />

        {/* Directional light mimics the sun and projects shadows */}
        <directionalLight
          position={[5, 10, 5]}
          intensity={1.5}
          castShadow
          shadow-mapSize-width={1024}
          shadow-mapSize-height={1024}
        />

        {/* Custom 3D Object we will create next */}
        <SpinningCube position={[-1.2, 0, 0]} />
        <SpinningCube position={[1.2, 0, 0]} />

        {/* Adds interactive Orbit Controls */}
        <OrbitControls enableZoom={true} maxPolarAngle={Math.PI / 2} />
      </Canvas>
    </div>
  );
}

Step 2: Designing an Interactive Mesh Component

Now we will create the <SpinningCube /> component. We want this cube to:

  1. Spin continuously on its axes.
  2. Scale up smoothly when hovered.
  3. Change its color when clicked.

To achieve continuous animations, we use R3F's useFrame hook. This hook subscribes to the browser's render loop (requestAnimationFrame) and exposes the state of the renderer, camera, and timer.

Create the component: app/components/SpinningCube.tsx:

"use client";

import { useRef, useState } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";

interface CubeProps {
  position: [number, number, number];
}

export default function SpinningCube({ position }: CubeProps) {
  // Reference the underlying Three.js Mesh instance directly
  const meshRef = useRef<THREE.Mesh>(null);

  // Component React states for interactive hover and click actions
  const [hovered, setHover] = useState(false);
  const [active, setActive] = useState(false);

  // Hook run on every frame update (roughly 60-120fps)
  useFrame((state, delta) => {
    if (meshRef.current) {
      // Rotate the cube continuously
      meshRef.current.rotation.x += delta * 0.5;
      meshRef.current.rotation.y += delta * 0.8;

      // Smoothly interpolate the scale using MathUtils lerp
      const targetScale = hovered ? 1.3 : 1.0;
      meshRef.current.scale.x = THREE.MathUtils.lerp(meshRef.current.scale.x, targetScale, 0.1);
      meshRef.current.scale.y = THREE.MathUtils.lerp(meshRef.current.scale.y, targetScale, 0.1);
      meshRef.current.scale.z = THREE.MathUtils.lerp(meshRef.current.scale.z, targetScale, 0.1);
    }
  });

  return (
    <mesh
      ref={meshRef}
      position={position}
      castShadow
      receiveShadow
      onClick={() => setActive(!active)}
      onPointerOver={() => setHover(true)}
      onPointerOut={() => setHover(false)}
    >
      {/* 3D Geometry details: Box width, height, and depth */}
      <boxGeometry args={[1, 1, 1]} />

      {/* 3D Material: MeshStandardMaterial requires lighting to see textures */}
      <meshStandardMaterial
        color={active ? "#2dd4bf" : hovered ? "#f43f5e" : "#6366f1"}
        roughness={0.2}
        metalness={0.8}
      />
    </mesh>
  );
}

Step 3: Integrating the 3D Component into Next.js

Since Three.js relies heavily on client-side window properties, rendering it server-side will cause hydration errors. We should load our ThreeScene dynamically, disabling SSR.

Update app/page.tsx:

"use client";

import dynamic from "next/dynamic";

// Dynamically import the ThreeScene component without SSR
const ThreeScene = dynamic(() => import("./components/ThreeScene"), {
  ssr: false,
  loading: () => (
    <div className="w-full h-screen bg-slate-950 flex items-center justify-center">
      <div className="text-center space-y-4">
        <div className="w-12 h-12 border-4 border-teal-500 border-t-transparent rounded-full animate-spin mx-auto"></div>
        <p className="text-teal-400 font-mono text-xs animate-pulse">Initializing WebGL Engine...</p>
      </div>
    </div>
  ),
});

export default function Home() {
  return (
    <main className="w-screen h-screen overflow-hidden">
      <ThreeScene />
    </main>
  );
}

Optimization Techniques for Production

Rendering WebGL can be resource-intensive, especially on mobile devices. Consider the following optimizations:

  1. Re-use Geometries and Materials: If you have thousands of objects (like trees in a forest), don't declare <boxGeometry /> inside each mesh map loop. Instantiate the geometry once outside and pass it down as a reference.
  2. Limit Pixel Ratio: Retaining a device pixel ratio of 3x or 4x on high-res displays can kill frame rates. Clamp it to a maximum of 2 using R3F's Canvas helper:
    <Canvas dpr={[1, 2]} />
    
  3. Use the Bvh Component: For scenes with complex collision triggers or raycasting, wrap the components in Drei's <Bvh> to speed up mouse-interaction calculations.

Conclusion

You have constructed a fully interactive, responsive 3D WebGL scene using React Three Fiber and Drei! Rather than writing hundreds of lines of imperative Three.js logic to setup animation tickers, resizing algorithms, and event handling, R3F lets you construct standard, declarative React states.

From here, you can extend your scene by importing custom GLTF/GLB models using Drei's useGLTF hook, applying post-processing shaders, or introducing physics engines like Rapier to achieve realistic collisions.

Three.jsReact Three FiberWebGL3D GraphicsReact

Related Posts

Interested in working together?

Let's translate complex software and automation problems into clean, high-performance systems.