← Tutorials
BUILD · 3D · 6 MIN

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.

For
builders who want a 3D touch on their site and work with AI
Needs
Node, Claude Code, and a React or Astro site (make one here)
Time
minutes for the scene, an hour to make it yours
Live, rendered right here by the ~40 lines below. Drag to orbit.

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 mesh + lights CAMERA your viewpoint RENDERER every frame <canvas> what you see the loop runs this ~60×/second
Six nouns. The scene and camera feed the renderer, the renderer paints the canvas, the loop reruns it every frame.

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 moat around 3D was never the concepts. It was the grind, and the grind is exactly the part that got automated.

When something breaks

Now try this

The hero spins. A few ways to make it yours, each a sentence to Claude:

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.

FOUND THIS USEFUL? GET THE NEXT ONE

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 ↗