A 3D hero with React Three Fiber
A spinning, brushed-metal 3D hero, on a dark page, branded with your name. It took minutes, not the afternoon I budgeted. The surprise isn't that it looks good. It's that 3D on the web stopped being a specialist's grind, and almost nobody noticed.
I sat down to learn Three.js expecting to lose an afternoon to it. I had a rendered, spinning hero with my name on it before the coffee was cold. That gap, between what 3D used to cost and what it costs now, is the whole story. The math of WebGL never got easier. The grind around it got deleted.
The one idea that unlocks it: a film set
Every Three.js scene is the same six things. Once they click, every tutorial and every snippet Claude hands you reads cleanly, and a broken render becomes a ten-second fix instead of a mystery.
- Scene — the container that holds everything. The set.
- Mesh — a visible thing, made of a Geometry (the shape) plus a Material (the surface). The actor: body plus costume.
- Light — without it, a standard material is just black. The studio lights.
- Camera — your point of view and lens. Where you stand to look.
- Renderer — draws the scene and camera onto a
<canvas>. The film. - The loop — redraw every frame so things move. Running the camera.
1. The smallest thing that renders
Here is the whole model in one file, no build step. Three.js comes straight off a CDN, so this runs by opening the page. Notice the six nouns going by in order:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene(); // 1. the set
const camera = new THREE.PerspectiveCamera(
50, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 0, 4.5); // 2. where you stand Show the full sceneShow less
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(innerWidth, innerHeight); // 3. the film
document.body.appendChild(renderer.domElement);
const knot = new THREE.Mesh( // 4. the actor
new THREE.TorusKnotGeometry(1, 0.32, 240, 32), // shape (geometry)
new THREE.MeshStandardMaterial({ // surface (material)
color: 0x9aa0a6, metalness: 0.9, roughness: 0.22 })
);
scene.add(knot);
scene.add(new THREE.AmbientLight(0xffffff, 0.25)); // 5. lights
const key = new THREE.DirectionalLight(0xffffff, 2.4);
key.position.set(3, 3, 4);
scene.add(key);
const controls = new OrbitControls(camera, renderer.domElement);
controls.autoRotate = true;
renderer.setAnimationLoop(() => { // 6. the loop
controls.update();
renderer.render(scene, camera);
}); That is it. Build the set, place the actor and the lights, stand the camera, run the loop. A black screen almost always means one of two things: a material that needs light with no light in the scene, or the camera sitting inside the object. Add a light, pull the camera back, and look again.
2. The same scene in React Three Fiber
The vanilla version is great for understanding the model. For a real site on a
React or Astro stack, you want React Three Fiber (R3F): the same Three.js
underneath, but the scene is written as components instead of imperative setup.
The drei helper library hands you cameras, controls, and loaders for
free. Install them:
npx astro add react
npm i three @react-three/fiber @react-three/drei Then the exact scene from above becomes this:
// Hero3D.jsx
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
function Knot() {
return (
<mesh>
<torusKnotGeometry args={[1, 0.32, 240, 32]} />
<meshStandardMaterial color="#9aa0a6" metalness={0.9} roughness={0.22} />
</mesh>
);
} Show the full componentShow less
export default function Hero3D() {
return (
<Canvas camera={{ position: [0, 0, 4.5], fov: 50 }}>
<ambientLight intensity={0.25} />
<directionalLight position={[3, 3, 4]} intensity={2.4} />
<Knot />
<OrbitControls autoRotate enablePan={false} />
</Canvas>
);
}
Read it next to the vanilla file and the trade is obvious: the JSX is the
scene graph. <mesh> wraps a geometry and a material, lights are
tags, and there is no manual loop to wire. That is the whole argument for R3F once
you are on React.
3. Drop it into your site
In Astro, a 3D canvas has to run in the browser, so render the component as a client island. One import, one tag:
---
import Hero3D from '../components/Hero3D.jsx';
---
<Hero3D client:only="react" /> client:only="react" tells Astro to skip server rendering for this and
boot it on the client, which is what a live WebGL canvas needs. Style the
<Canvas> to fill its container, drop a headline over it, and you
have a hero.
What changed, and why this took minutes
I budgeted an afternoon because that is what 3D on the web used to cost: fighting boilerplate, context loss, and cryptic errors before anything appeared. None of that is the hard part anymore.
- The boilerplate is gone. Claude already knows the whole Three.js and R3F surface. You describe the scene, it writes the setup, you adjust by talking. The skill that's left is the six-noun model, so you can debug what comes back.
- WebGPU is here, and you can ignore it for now. Since Three.js r171 the faster WebGPU renderer ships with an automatic WebGL fallback, so ~95% of browsers are covered. It matters for heavy scenes, particles, thousands of objects. For a hero, plain WebGL is fine. Know the word exists, don't start there.
The moat around 3D was never the concepts. It was the grind, and the grind is exactly the part that got automated.
When something breaks
- Black screen. No light plus a
MeshStandardMaterial, or the camera is inside the object. Add anambientLightand move the camera back before touching anything else. - "Failed to resolve module three". The vanilla version needs an import map, or you skipped the npm install in the R3F version. Use one path or the other, not half of each.
- Canvas is blank in Astro but fine standalone. You forgot
client:only="react", so it tried to render WebGL on the server. Add it. - It renders, then freezes. Something throws inside the loop. Open the browser console; the error and line are right there. Paste it into Claude.
Now try this
The hero spins. A few ways to make it yours, each a sentence to Claude:
- Swap the shape. Ask for an icosahedron, a loaded
.glbmodel, or your logo extruded into 3D. - Match your brand. Change the material color and the light colors to your palette; a cool rim light reads as expensive.
- React to the mouse. Have the shape tilt toward the cursor instead of auto-spinning.
- Keep it cheap. One mesh and two lights stays light on any laptop. Save the WebGPU renderer for the day you actually have thousands of objects.
What you just did
A real 3D element, on a real page, in the time it takes to read this. What matters isn't Three.js or R3F. It's that the thing that used to make 3D a specialty, the setup grind, is the thing AI removed. The concepts are small. The barrier was the labor, and the labor is gone.
Drop your email and I'll ping you when the next build goes up, with the code and prompts I used. No spam.
More tutorials ↗