From Blender to Browser

#react#web#blender

Let’s explore how to build a fully interactive 3D scene on the web using React, Three.js, and Blender.

Whether you're creating a portfolio, game environment, or immersive brand experience, this guide walks you through modeling in Blender, exporting to GLTF, rendering in the browser, and hooking up custom interactivity with ReactJS.

Tech Stack

TechDescription
Next.jsReact framework for the web
Three.jsWebGL based 3D engine
Blender3D modeling & animation tool

Example Scene

Disclaimer: Everything you see in this room was created by me without vibe-coding. :flex:


Let's explore!

  • Set up a basic 3D scene in the browser using Three.js and React
  • Export models from Blender
  • Trigger animations and build interactive experiences with event handling

0. Project Setup

Scaffold project folder with npx and nextjs

three_init CLI

# Create project folder
npx create-next-app@latest three-demo
 
# Navigate to it
cd three-demo
 
# Make scene file
touch pages/scene.tsx
 
# Install dependencies
npm install three
npm i --save-dev @types/three

1. Building the 3D Scene with Three.js

We’ll start by initializing a minimal scene with a simple environment, a camera, a rotating cube.

pages/index.tsx

import Link from 'next/link';
 
export default function Home() {
    return (
        <main style={{ padding: '2rem' }}>
            <h1>Welcome to the 3D Demo</h1>
            <Link href="/scene">Go to Scene</Link>
        </main>
    );
}

pages/scene.tsx

// pages/scene.tsx
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
 
export default function ScenePage() {
    const mountRef = useRef<HTMLDivElement>(null);
 
    useEffect(() => {
        const width = mountRef.current?.clientWidth ?? window.innerWidth;
        const height = mountRef.current?.clientHeight ?? window.innerHeight;
 
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
        camera.position.z = 2;
 
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(width, height);
        mountRef.current?.appendChild(renderer.domElement);
 
        const geometry = new THREE.BoxGeometry();
        const material = new THREE.MeshNormalMaterial();
        const cube = new THREE.Mesh(geometry, material);
        scene.add(cube);
 
        function animate() {
            cube.rotation.x += 0.01;
            cube.rotation.y += 0.01;
            renderer.render(scene, camera);
            requestAnimationFrame(animate);
        }
        animate();
 
        return () => {
            renderer.dispose();
            mountRef.current?.removeChild(renderer.domElement);
        };
    }, []);
 
    return <div ref={mountRef} style={{ width: '100%', height: '99vh' }} />;
}

Or...clone the repo at branch: 01-init


2. The Blender → Browser Pipeline

Blender is our modeling and animation hub. We’ll use the .glb format for exporting clean, optimized models with animations baked in.

For a Blender Export Test:

  1. Make a new file
  2. Remove everything from the scene with AX → "Delete"
  3. Make a Monkey! Shift + AMeshMonkey
  4. Make it gold! Add a material
    • Hex: #FFD700FF
    • Metallic: 1
    • Roughness: 0.2
  5. Export settings:
    • Format: .glb
    • Transform: +Y Up this should be checked
    • Name meshes in the Outline for referencing programmatically

Copy Draco Decoder Binariespublic/jsm/libs/draco/

// Load GLTF model
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/jsm/libs/draco/');
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load('/models/suzanne.glb', (gltf) => {
    console.log("loading suzanne...")
    scene.add(gltf.scene);
});
 
// Debug
scene.add(new THREE.GridHelper(10, 10)); // size, divisions
scene.add(new THREE.AxesHelper(5)); // length of x/y/z axes
 
// Orbit Controls
orbitControls = new OrbitControls(camera, rendererManager.domElement);
orbitControls.minDistance = 2.0;
orbitControls.maxDistance = 10.0;
orbitControls.enableDamping = true;
orbitControls.target.copy(new THREE.Vector3(0, 0, 0));

Or...clone the repo at branch: 02-glb-import


3. Making It Interactive

Now let’s give users something to do. We can add clickable areas to meshes with raycasting and onPointerDown

// Raycasting
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
 
// Click Interaction
const onPointerDown = (event: PointerEvent) => {
    const rect = renderer.domElement.getBoundingClientRect();
    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
 
    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(scene.children, true);
 
    const suzanneHit = intersects.find((hit) => hit.object.name === 'Suzanne');
    if (suzanneHit && suzanneMesh) {
        isSpinning = !isSpinning;
        if (isSpinning) {
            spinTarget = suzanneMesh.rotation.y + Math.PI * 2 * 5; // one full spin
            spinProgress = 0;
        }
        console.log(`🌀 Suzanne ${isSpinning ? 'started' : 'stopped'} spinning`);
    }
};

Or...clone the repo at branch: 03-interactivity

4. Hooking Up Blender Animations

Time to make your scene really come alive.

  • Keyframing animations in Blender
  • Loading animation clips via useAnimations
  • Triggering them with React state and refs

When loading the .glb import the animations to the mixer so can loop over them onClick to play.

 
gltfLoader.load('/models/suzanne2.glb', (gltf) => {
    gltf.scene.traverse((child) => {
        if ((child as THREE.Mesh).isMesh && child.name === 'Suzanne') {
            suzanneMesh = child;
            console.log('✅ Suzanne mesh found');
        }
    });
    scene.add(gltf.scene);
 
    // Load animations to mixer
    if (gltf.animations.length > 0) {
        mixer = new THREE.AnimationMixer(gltf.scene);
        animationActions = gltf.animations.map((clip) => {
            const action = mixer.clipAction(clip);
            action.setLoop(THREE.LoopOnce, 1);
            action.clampWhenFinished = true;
            return action;
        });
 
        console.log('✅ Animations loaded:', gltf.animations.map((a) => a.name));
    }
});
 
// Animate
const animate = () => {
    requestAnimationFrame(animate);
 
    if (mixer) mixer.update(0.016); // or use delta time with THREE.Clock
 
    orbitControls.update();
    renderer.render(scene, camera);
};
 
animate();

Or...clone the repo at branch: 04-animations


5. UX Tips for 3D Web Design

3D is powerful, but easy to overdo. Here's how to keep it usable.

  • Don’t animate everything; focus user attention
  • Camera limits: orbit controls should feel intentional

6. Final Touches

Structure matters for scale and maintenance.

  • Organizing GLTF assets and code modules
  • Performance: reduce poly count, compress textures
  • Accessibility: fallback for touch devices, no-WebGL
  • Decoupling scene logic from UI state

--

Conclusion

You now have the tools to create rich, animated, and interactive 3D web scenes.

Link to Live Demo


Try It Yourself

DM me if you build something cool, I'd love to see it.