How to set up Three.js with Next.js and TypeScript in 2025

How to Set Up Three.js with Next.js and TypeScript in 2025

Building interactive 3D web experiences requires more than just throwing Three.js into your project. When combining Three.js with Next.js and TypeScript, you'll encounter module resolution quirks, server-side rendering gotchas, and canvas initialization timing issues. This guide walks you through a production-ready setup that avoids common pitfalls.

Why Three.js + Next.js?

Three.js is the go-to JavaScript 3D library for cross-browser WebGL and WebGPU rendering. Next.js provides modern React patterns, optimized bundling, and API routes. Combining them gives you:

  • SSR-safe 3D rendering (canvas only initializes client-side)
  • TypeScript type safety for geometries, materials, and cameras
  • Optimized builds with Next.js's tree-shaking and code splitting
  • Fast refresh for iterating on 3D scenes during development

Prerequisites

  • Node.js 18+ installed
  • Basic knowledge of React and Next.js
  • Familiarity with ES6 imports

Step 1: Create a Next.js Project with TypeScript

Start with a fresh Next.js installation:

npx create-next-app@latest my-threejs-app --typescript --tailwind
cd my-threejs-app

When prompted, select:

  • ESLint: Yes
  • src/ directory: Yes (optional but recommended)
  • App Router: Yes

Step 2: Install Three.js and Types

Three.js ships as an ESM module on npm, making it Next.js-compatible by default:

npm install three
npm install --save-dev @types/three

Verify installation:

npm list three
# three@r128 (or latest version)

Step 3: Create a Client-Side 3D Component

Since Three.js requires a browser DOM, wrap it in a client component. Create src/components/Canvas3D.tsx:

'use client';

import { useEffect, useRef } from 'react';
import * as THREE from 'three';

export default function Canvas3D() {
  const containerRef = useRef<HTMLDivElement>(null);
  const sceneRef = useRef<THREE.Scene | null>(null);
  const rendererRef = useRef<THREE.WebGLRenderer | null>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    // Scene setup
    const width = window.innerWidth;
    const height = window.innerHeight;

    const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10);
    camera.position.z = 1;

    const scene = new THREE.Scene();
    sceneRef.current = scene;

    // Create a rotating cube
    const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
    const material = new THREE.MeshNormalMaterial();
    const mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);

    // WebGL renderer
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(width, height);
    renderer.setPixelRatio(window.devicePixelRatio);
    rendererRef.current = renderer;

    containerRef.current.appendChild(renderer.domElement);

    // Animation loop
    const animate = (time: number) => {
      mesh.rotation.x = time / 2000;
      mesh.rotation.y = time / 1000;
      renderer.render(scene, camera);
    };

    renderer.setAnimationLoop(animate);

    // Handle resize
    const handleResize = () => {
      const w = window.innerWidth;
      const h = window.innerHeight;
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      renderer.setSize(w, h);
    };

    window.addEventListener('resize', handleResize);

    // Cleanup
    return () => {
      window.removeEventListener('resize', handleResize);
      renderer.dispose();
      geometry.dispose();
      material.dispose();
      containerRef.current?.removeChild(renderer.domElement);
    };
  }, []);

  return <div ref={containerRef} className="w-full h-screen" />;
}

Step 4: Use the Component in Your Page

Update src/app/page.tsx:

import Canvas3D from '@/components/Canvas3D';

export default function Home() {
  return (
    <main className="w-full h-screen">
      <Canvas3D />
    </main>
  );
}

Step 5: Verify Your Build

Start the development server:

npm run dev

Open http://localhost:3000 in your browser. You should see a rotating cube that responds to window resizing.

For production, verify bundle size:

npm run build

Three.js typically adds ~600KB gzipped, depending on which modules you import.

Common Configuration Issues

Issue: "Module not found: three"

Solution: Ensure you're in a client component ('use client' directive). Three.js requires browser APIs and cannot run server-side.

Issue: Canvas doesn't resize on window resize

Solution: Always update the camera's aspect ratio and call updateProjectionMatrix() when the window resizes, as shown in the example above.

Issue: Memory leaks or poor performance

Solution: Call .dispose() on geometries, materials, and renderers in cleanup functions. This frees WebGL resources.

Advanced Setup: Loaders and Assets

For GLTF models, use Three.js's built-in loader:

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

const loader = new GLTFLoader();
loader.load('/model.glb', (gltf) => {
  scene.add(gltf.scene);
});

Place your 3D assets in public/ for Next.js to serve them statically.

TypeScript Tips

Always type your refs and state:

const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);

This catches type mismatches at build time, not runtime.

Next Steps

  • Explore Three.js examples for inspiration
  • Learn about materials: MeshStandardMaterial, MeshPhongMaterial for realistic lighting
  • Add controls with OrbitControls from three/examples/jsm/controls/OrbitControls
  • Consider using Drei (React Three Fiber) for a more React-friendly API

Your Three.js + Next.js + TypeScript stack is now production-ready. Happy rendering!

Recommended Tools

  • VercelDeploy frontend apps instantly with zero config
  • GitHubWhere the world builds software