A step-by-step workflow for adding real 3D to your website using React Three Fiber, Three.js, and Blender — from stack choice to deployment.
Most "3D websites" you see on Awwwards aren't magic. They're a handful of predictable pieces stacked together: a rendering engine, a lightweight model, some interaction hooks, and a lot of performance discipline. This is the workflow I use when I want to ship a real 3D experience without turning my Lighthouse score into a crime scene.
You don't need to touch raw WebGL. Here's how the options stack up:
For most dev-facing sites, R3F + drei (a helper library of common utilities) is the sweet spot. It's production-ready, has a large ecosystem, and doesn't require you to relearn how to write React.
npx create-next-app@latest my-3d-site
cd my-3d-site
npm install three @react-three/fiber @react-three/drei
Your 3D scene lives inside a <Canvas> component, which slots into your normal component tree like any other element:
import { Canvas } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
export default function Scene() {
return (
<Canvas camera={{ position: [0, 0, 5] }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
<OrbitControls />
</Canvas>
)
}
That's a lit, rotatable cube in about 15 lines. Everything from here is layering complexity on top of this pattern.
Model in Blender (free), export as .glb, then load it with drei's useGLTF:
import { useGLTF } from '@react-three/drei'
function Model() {
const { scene } = useGLTF('/model.glb')
return <primitive object={scene} />
}
Wrap it in Suspense since the model loads asynchronously:
<Suspense fallback={null}>
<Model />
</Suspense>
Tip: run your .glb through gltf.report or gltf-transform to compress it with Draco before shipping. A model that's 40MB uncompressed can often drop under 5MB with zero visible quality loss.
Two hooks do most of the work:
useFrame — runs on every rendered frame, for animation loops (rotation, floating, pulsing)onClick, onPointerOver) — for hover states and click interactions directly on meshesuseFrame((state, delta) => {
meshRef.current.rotation.y += delta * 0.5
})
This is the part that separates a portfolio demo from a site people can actually use on a mid-range phone:
<Canvas dpr={[1, 2]}> to avoid retina rendering overkillVercel or Netlify handle this with zero special config. The one thing worth watching is bundle size — Three.js itself is heavy, so run @next/bundle-analyzer before you ship and lazy-import the 3D component if it's not needed on every route.
The gap between "cool WebGL demo" and "3D website people actually use" is almost entirely performance work, not creative work. Get the cube spinning first, then spend your remaining time making sure it doesn't melt someone's phone.
Building this in public — if you try this workflow, tag me @code.akshat.in.

Hey, I'm Akshat — a full-stack dev, AI tinkerer, and relentless builder who documents every step of the journey. I share what I learn in real-time — dev tutorials, design insights, and AI + tech news.
Comments