Use Cases (Updated: 6/2/2026)

Build Practical Three.js 3D Web UI with Claude Code

Use Claude Code and Three.js to build a real 3D viewer with resize, cleanup, review prompts, and use cases.

Build Practical Three.js 3D Web UI with Claude Code

Three.js makes WebGL approachable, but a production 3D interface is still more than a spinning object. You need a camera that frames the subject, lighting that reveals shape, a canvas that resizes correctly, GPU settings that do not punish mobile devices, and cleanup code that releases geometries, materials, controls, and the renderer when the React component unmounts.

Claude Code is useful because it can generate the repetitive Three.js scaffolding quickly. The risk is that a vague request such as “make this 3D” often produces a nice demo and misses the operational details. In a real product viewer, data visualization, or educational scene, those details decide whether the feature can stay in production.

This guide shows a copyable Vite + React + Three.js starting point and the review prompts I would give Claude Code before shipping. It is written for frontend developers who know React and want a practical way to add 3D without turning the page into a fragile showcase.

Start With Product Requirements

Before asking Claude Code to write code, define what the user must learn from the 3D scene. For an ecommerce product viewer, the goal might be color, scale, backside details, and material finish. For a data visualization, the goal might be comparing clusters or time-based movement. For a portfolio or educational scene, the goal might be guiding attention from one part to another.

Claude Code works best when the request contains constraints, not only aesthetics. A strong first prompt looks like this:

Create a Vite + React + TypeScript + three product viewer.
Requirements:
- render the canvas inside a parent element, not directly on document.body
- resize with the parent container
- enable rotate and zoom with OrbitControls
- dispose geometry, material, renderer, and controls on unmount
- cap devicePixelRatio at 2 for mobile GPUs
- keep the code copyable into src/App.tsx

These requirements form a harness, meaning the working frame that lets the agent produce code safely. The harness is not glamorous, but it prevents the common failures: blank canvas, stretched rendering, memory leaks after navigation, and GPU load that is invisible on a developer laptop but painful on a phone.

For API details, keep the Three.js documentation open. Renderer behavior in particular is easier to verify against the official WebGLRenderer documentation than by guessing from examples.

Minimal Vite/React Setup

Start with plain Three.js inside React before adding React Three Fiber or post-processing. That makes lifecycle problems obvious. Once the raw renderer, camera, lights, resize handler, and disposal path are clear, moving to higher-level libraries becomes a deliberate choice rather than a way to hide complexity.

npm create vite@latest three-claude-demo -- --template react-ts
cd three-claude-demo
npm i three
npm run dev

The first version should not depend on a GLB model, HDR environment, shader pipeline, or CMS asset path. A simple mesh is enough to prove that the viewer works. After that, you can replace the mesh with a real product model.

flowchart LR
  A["React component"] --> B["mount div"]
  B --> C["WebGLRenderer canvas"]
  C --> D["Scene"]
  D --> E["Camera and lights"]
  D --> F["Mesh and material"]
  C --> G["OrbitControls"]
  G --> H["resize and dispose"]

Copyable 3D Viewer

Paste this into src/App.tsx. It creates a small product-like object, adds lighting and controls, resizes with the container, and releases Three.js resources when React unmounts the component.

import { useEffect, useRef } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import "./App.css";

export default function App() {
  const mountRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    const mount = mountRef.current;
    if (!mount) return;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0xf6f7fb);

    const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
    camera.position.set(3.5, 2.2, 4.5);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.shadowMap.enabled = true;
    mount.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.minDistance = 2.5;
    controls.maxDistance = 8;
    controls.target.set(0, 0.4, 0);

    const ambient = new THREE.HemisphereLight(0xffffff, 0x7c8594, 1.6);
    scene.add(ambient);

    const keyLight = new THREE.DirectionalLight(0xffffff, 2.4);
    keyLight.position.set(3, 5, 4);
    keyLight.castShadow = true;
    scene.add(keyLight);

    const productGeometry = new THREE.BoxGeometry(1.8, 1.2, 1.1, 4, 4, 4);
    const productMaterial = new THREE.MeshStandardMaterial({
      color: 0x2f6f73,
      roughness: 0.42,
      metalness: 0.08,
    });
    const product = new THREE.Mesh(productGeometry, productMaterial);
    product.castShadow = true;
    product.position.y = 0.75;
    scene.add(product);

    const floorGeometry = new THREE.CircleGeometry(2.2, 64);
    const floorMaterial = new THREE.MeshStandardMaterial({
      color: 0xd9dee8,
      roughness: 0.7,
    });
    const floor = new THREE.Mesh(floorGeometry, floorMaterial);
    floor.rotation.x = -Math.PI / 2;
    floor.receiveShadow = true;
    scene.add(floor);

    const resize = () => {
      const width = mount.clientWidth;
      const height = mount.clientHeight;
      if (width === 0 || height === 0) return;

      camera.aspect = width / height;
      camera.updateProjectionMatrix();
      renderer.setSize(width, height, false);
    };

    let frameId = 0;
    const clock = new THREE.Clock();

    const animate = () => {
      const elapsed = clock.getElapsedTime();
      product.rotation.y = elapsed * 0.45;
      product.rotation.x = Math.sin(elapsed * 0.8) * 0.08;
      controls.update();
      renderer.render(scene, camera);
      frameId = window.requestAnimationFrame(animate);
    };

    resize();
    animate();
    window.addEventListener("resize", resize);

    return () => {
      window.removeEventListener("resize", resize);
      window.cancelAnimationFrame(frameId);
      controls.dispose();

      scene.traverse((object) => {
        if (object instanceof THREE.Mesh) {
          object.geometry.dispose();
          const materials = Array.isArray(object.material)
            ? object.material
            : [object.material];
          materials.forEach((material) => material.dispose());
        }
      });

      renderer.dispose();
      renderer.domElement.remove();
    };
  }, []);

  return (
    <main className="viewerShell">
      <div className="copy">
        <p className="eyebrow">Three.js + Claude Code</p>
        <h1>3D product viewer</h1>
        <p>
          Drag to rotate, scroll to zoom, and resize the window to verify that
          the canvas follows its container.
        </p>
      </div>
      <div ref={mountRef} className="viewerStage" />
    </main>
  );
}

Add this to src/App.css. The key detail is that .viewerStage has a real height. A renderer can be initialized perfectly and still appear blank if its parent collapses to zero height.

body {
  margin: 0;
  font-family: Inter, system-ui, sans-serif;
  background: #eef2f7;
  color: #17202a;
}

.viewerShell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: minmax(260px, 0.8fr) minmax(320px, 1.2fr);
  gap: 32px;
  align-items: center;
  padding: 40px;
  box-sizing: border-box;
}

.copy {
  max-width: 520px;
}

.eyebrow {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #2f6f73;
}

.copy h1 {
  margin: 0 0 16px;
  font-size: clamp(36px, 5vw, 64px);
  line-height: 1;
}

.copy p {
  font-size: 17px;
  line-height: 1.7;
}

.viewerStage {
  height: min(62vh, 560px);
  min-height: 360px;
  border: 1px solid #ccd5df;
  background: #f6f7fb;
}

.viewerStage canvas {
  display: block;
}

@media (max-width: 760px) {
  .viewerShell {
    grid-template-columns: 1fr;
    padding: 24px;
  }

  .viewerStage {
    min-height: 300px;
  }
}

Ask Claude Code To Review The Risk

After the first implementation, ask Claude Code to review the code against production risks. This is more valuable than asking it to “polish” the scene.

Review this Three.js React implementation.
Check:
1. the canvas follows its parent size
2. geometry, material, renderer, and controls are disposed on unmount
3. requestAnimationFrame stops on unmount
4. devicePixelRatio is safe for high-density mobile screens
5. window/document access will not break SSR frameworks such as Next.js or Astro
6. camera, lighting, and controls support product inspection
Return concrete diffs and a manual test checklist.

This turns Claude Code into a second reviewer. It is especially helpful for dispose paths, because memory leaks do not always show up in a quick visual check. Navigate away from the page several times, watch memory in DevTools, and confirm that the app does not keep old renderers alive.

Three Useful Use Cases

Use caseWhy 3D helpsWhat to ask Claude Code to build
3D product viewerCustomers can inspect depth, color, finish, and hidden sides before purchaseOrbitControls, color variants, lighting presets, mobile performance checks
Data visualizationSpatial structure can reveal clusters, outliers, or time-based movementpoint clouds, 3D bars, camera transitions, legends, selection states
Portfolio or educational sceneLearners can rotate an object while explanations guide attentionlabels, highlighted parts, guided camera positions, paused auto-rotation

A product viewer should reduce purchase anxiety, not merely rotate a model. If the product is furniture, show the bottom and footprint. If it is hardware, show ports and scale. If it is a wearable, test lighting that reveals material without exaggerating the color.

For data visualization, 3D is not automatically better. Bars can hide other bars, perspective can distort comparisons, and free camera movement can make analysis harder. Ask Claude Code to keep the comparison task explicit and to provide a 2D fallback when depth does not add value.

For portfolios and education, the missing feature is often sequencing. Add buttons that move the camera to planned viewpoints, labels that appear at the right time, and an option to pause auto-rotation while text is being read.

Common Failures And Fixes

FailureLikely causeFix
Blank canvasparent height is zero, camera misses the mesh, or no light reaches the materialset CSS height, verify camera position and controls target, add a simple light
Stretched scene after resizecamera aspect and renderer size are not updated togethercall camera.updateProjectionMatrix() and renderer.setSize() in one resize function
Page gets slower after navigationgeometries, materials, controls, or renderer are not disposedtraverse scene meshes on unmount and dispose every resource
Phone overheatshigh pixel ratio, expensive shadows, or too many segmentscap pixel ratio at 2, reduce shadows, reduce geometry detail
SSR crashcode touches window or document during server renderinitialize Three.js inside useEffect or a client-only component

When the canvas is blank, remove complexity first. Use a bright background, one cube, one light, and a camera you can reason about. Do not debug a GLB loader, HDR environment, post-processing stack, and layout issue at the same time.

Performance And Shipping Checklist

Before release, test on the weakest device you expect to support. A scene that looks smooth on a desktop GPU can be unpleasant on a midrange phone. Check frame rate, battery impact, first load size, and whether the user can still complete the main task if 3D fails.

Keep these checks in the pull request:

  • target phones maintain acceptable frame rate
  • a lower-cost mode exists for shadows, texture size, or model detail
  • a fallback image appears when WebGL or the model asset fails
  • keyboard users and screen reader users get an equivalent explanation
  • model files are compressed and do not block the first useful paint

For related canvas architecture, see the Claude Code canvas development guide. For motion tuning, the Claude Code animation implementation guide is a useful companion.

Claude Code Lab CTA

Claude Code Lab can help review 3D product viewers, WebGL data visualizations, and educational scenes before they become hard to maintain. The most useful inputs are target devices, framework constraints, model format, current performance budget, and the business reason for using 3D.

For team training, I recommend standardizing review prompts. Instead of judging generated 3D code by appearance, check resize behavior, cleanup, accessibility, performance, and alignment with official documentation every time.

Results From Testing

I tested the sample by pasting it into a Vite React TypeScript project, resizing the browser, and switching between desktop and mobile widths. The explicit stage height prevented the common blank-canvas layout bug. Capping devicePixelRatio at 2 reduced unnecessary GPU work on high-density screens, and asking Claude Code to review disposal paths made it easier to catch cleanup omissions before the code felt production-ready.

#Claude Code #Three.js #3D #WebGL #frontend
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.