Skip to content

Three.js - Responsive Design

A modern web experience must be responsive, meaning it should look and function well on any screen size, from a large desktop monitor to a small smartphone. A static Three.js scene will appear cropped or stretched on different devices. This chapter covers the essential techniques to make your 3D scenes fully responsive.

By default, the <canvas> element where Three.js renders your scene has a fixed size. When the browser window is resized, two things need to be updated:

  1. The Renderer: It needs to know the new dimensions to draw to.
  2. The Camera: A perspective camera’s aspect ratio must match the new viewport dimensions to prevent the scene from looking squashed or stretched.

The solution is to listen for the browser’s resize event and update the necessary components within a handler function. Let’s set up a modern project structure first.

We’ll use a standard setup with separate HTML, CSS, and JavaScript files. It is highly recommended to use a package manager like npm and a build tool like Vite for managing dependencies and development.

# 1. Create a project folder and navigate into it
mkdir my-threejs-app
cd my-threejs-app
# 2. Initialize a node project and install dependencies
npm init -y
npm install three lil-gui
npm install --save-dev vite
# 3. Create your files
touch index.html style.css main.js

The HTML file is minimal. It provides a container for our canvas and loads our main script as a module.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Three.js Scene</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<canvas class="webgl"></canvas>
<script type="module" src="./main.js"></script>
</body>
</html>

The CSS ensures our canvas fills the entire viewport.

* {
margin: 0;
padding: 0;
}
html, body {
overflow: hidden; /* Prevent scrollbars */
}
.webgl {
position: fixed;
top: 0;
left: 0;
outline: none;
}

This is where the magic happens. We set up our scene and create the resize handler.

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// Sizes
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
// Scene
const scene = new THREE.Scene();
// Object
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1, 5, 5, 5),
new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })
);
scene.add(mesh);
// Camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.z = 3;
scene.add(camera);
// Renderer
const canvas = document.querySelector('canvas.webgl');
const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// --- RESIZE HANDLER ---
window.addEventListener('resize', () => {
// Update sizes
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// Update camera
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
// Update renderer
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
// Animation Loop
const tick = () => {
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
}
tick();

High-density screens (like Apple’s Retina displays) have more physical pixels than logical pixels. To get a crisp, sharp render on these screens, you must set the renderer’s pixel ratio. window.devicePixelRatio provides this value. We cap it at 2 with Math.min() because higher values offer diminishing visual returns while heavily impacting performance.

// Set pixel ratio for sharp rendering on high-DPI screens
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

Aliasing causes the edges of 3D objects to look jagged or “stair-stepped”. We can smooth these edges by enabling anti-aliasing when creating the renderer. Note that this comes with a performance cost, so use it judiciously.

// Enable anti-aliasing when instantiating the renderer
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
});
  • Forgetting camera.updateProjectionMatrix(): After changing the camera’s aspect ratio (or any other fundamental property like fov), you must call this method to apply the changes.
  • Performance: The resize event can fire very rapidly. For complex resize logic, you might consider “debouncing” the event handler to prevent it from running too frequently and causing performance issues.
  • Accessibility: A <canvas> element is not accessible to screen readers by default. Provide a fallback content for better accessibility: <canvas>A 3D visualization of a rotating cube.</canvas>