The hero on this site has a floating 3D sphere with a displacement effect that reacts to your cursor. The project pages show device mockups rendered in WebGL. When I first added these, I expected 3D to be a heavy lift — and it can be — but the fundamentals are surprisingly approachable once you know what you're working with.
This is a practical guide to Three.js in React: how the core concepts fit together, how to load and render 3D models, and the techniques I used specifically on this site.
The Three.js Mental Model
Three.js is a layer on top of WebGL, which is the browser's low-level 3D graphics API. Writing raw WebGL is verbose and unforgiving. Three.js handles the boilerplate and gives you a scene graph to work with.
Every Three.js scene has three ingredients:
- Scene — the container. Everything you render lives in the scene.
- Camera — your view into the scene. A
PerspectiveCameragives you a realistic field of view; anOrthographicCameragives you a flat projection useful for UI work. - Renderer — the WebGL context. It draws the scene from the camera's perspective onto a
<canvas>.
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75, // field of view in degrees
window.innerWidth / window.innerHeight, // aspect ratio
0.1, // near clipping plane
1000 // far clipping plane
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
Objects in the scene are Mesh instances — a geometry (shape) paired with a material (appearance):
const geometry = new THREE.SphereGeometry(1, 64, 64);
const material = new THREE.MeshStandardMaterial({ color: 0x8844ee });
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);
React Integration with @react-three/fiber
For React apps, @react-three/fiber is the standard way to use Three.js. It gives you a declarative JSX API over the Three.js scene graph — no imperative setup, no lifecycle management, no cleanup headaches.
npm install three @react-three/fiber @react-three/drei
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
function Scene() {
return (
<Canvas camera={{ position: [0, 0, 3], fov: 75 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardMaterial color="#8844ee" />
</mesh>
<OrbitControls />
</Canvas>
);
}
@react-three/drei is a collection of helpers — OrbitControls, Environment, useGLTF, Text, and many more — that cover most common needs.
Loading 3D Models
The GLTF format is the standard for web 3D models. It's compact, widely supported, and preserves materials and animations.
import { useGLTF } from '@react-three/drei';
function DeviceModel({ url }) {
const { scene } = useGLTF(url);
return <primitive object={scene} />;
}
For this site's device models, I use Draco compression to reduce file size significantly. Draco-compressed GLTF files need a decoder, which you ship as a static asset:
# Copy the decoder from the drei package to public/
cp -r node_modules/@react-three/drei/helpers/draco/ public/draco/
import { useGLTF } from '@react-three/drei';
// Configure the decoder path before using
useGLTF.preload('/models/device.glb');
function DeviceModel() {
const { scene } = useGLTF('/models/device.glb', '/draco/');
return <primitive object={scene} />;
}
The Displacement Sphere
The hero sphere on this site uses a displacement map — a texture that pushes vertices outward based on pixel brightness. Combined with cursor tracking, the effect makes the sphere look like it's reacting to the user's presence.
import { useRef } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { useTexture } from '@react-three/drei';
import * as THREE from 'three';
function DisplacementSphere() {
const meshRef = useRef();
const { viewport } = useThree();
const [displacementMap] = useTexture(['/textures/displacement.png']);
useFrame(({ mouse }) => {
if (!meshRef.current) return;
// Convert mouse position (-1 to 1) to subtle rotation
meshRef.current.rotation.x = mouse.y * 0.2;
meshRef.current.rotation.y = mouse.x * 0.2;
});
return (
<mesh ref={meshRef}>
<sphereGeometry args={[2, 128, 128]} />
<meshStandardMaterial
displacementMap={displacementMap}
displacementScale={0.4}
wireframe={false}
color="#8b5cf6"
roughness={0.4}
metalness={0.6}
/>
</mesh>
);
}
Higher segment counts (128, 128) give the displacement more geometry to work with — the effect looks smooth instead of faceted.
Lighting
Lighting makes or breaks a 3D scene. The most realistic starting point for product/portfolio work is using an HDR environment map for image-based lighting (IBL):
import { Environment } from '@react-three/drei';
function Scene() {
return (
<Canvas>
{/* HDR environment for realistic reflections and lighting */}
<Environment preset="city" />
<DeviceModel />
</Canvas>
);
}
@react-three/drei ships several presets. For custom HDRs, you can point Environment at a file in your public directory.
For more control, combine point lights and directional lights:
<ambientLight intensity={0.2} />
<pointLight position={[10, 10, 10]} intensity={1.5} color="#ffffff" />
<pointLight position={[-10, -5, -10]} intensity={0.5} color="#6644ee" />
A key light (primary source), fill light (softer secondary), and rim light (behind the subject, creates separation from background) is the classic three-point setup and works well in 3D too.
Performance
WebGL is expensive. A few things that matter:
Limit draw calls. Each separate mesh is a draw call. Merge static geometry where possible. Use instancing for repeated objects.
Use compressed textures. Basis Universal (.basis) and KTX2 are GPU-compressed formats that are faster to decompress and use less VRAM than PNG or JPEG. @react-three/drei has a KTX2Loader for this.
Reduce polygon count. Mobile GPUs struggle with high-poly models. Use a lower-poly model for mobile or reduce tessellation on geometry.
Suspend during load. Use React Suspense to show a fallback while models load. useGLTF already integrates with Suspense.
<Suspense fallback={<LoadingSpinner />}>
<DeviceModel />
</Suspense>
Dispose resources. Three.js doesn't garbage-collect textures and geometries automatically. With @react-three/fiber, the Canvas handles cleanup for JSX elements, but manually created resources need explicit disposal.
When to Reach for 3D
3D adds real visual weight to a site, but it also adds bundle size, performance cost, and complexity. The places it earns its keep:
- Hero sections where you want a memorable visual identity
- Product showcases where you want to show an object from all sides
- Data visualizations where 3D adds genuine insight (not just decoration)
- Interactions where the third dimension makes the concept clearer
For everything else, a well-executed 2D design is almost always faster to build, lighter to ship, and clearer to understand. 3D is a tool, not a flex — use it when it serves the experience, not just to show it can be done.
If you want to explore the implementation on this site, take a look at the source code.
