Related lab bench

Calculate the camera framing

Move from the guide to the camera calculator when you need a repeatable distance, field of view, or viewport coverage target.

Open Camera FOV Check before publishing

CSS size is not render size

A canvas has two sizes. CSS decides how large it appears on the page. The drawing buffer decides how many pixels WebGL renders. If these values drift apart, the scene can look blurry, stretched, or expensive. A good resize function reads the displayed size, updates the renderer drawing buffer, and then updates the camera projection.

Avoid setting canvas width and height in CSS alone and hoping Three.js will infer the rest. The renderer must know the real buffer size. At the same time, avoid letting `setSize` rewrite your CSS every frame. Pass `false` as the third argument so layout remains controlled by CSS.

Clamp pixel ratio

High-DPI screens can request two or three times as many pixels as the CSS size suggests. That can make a small phone canvas surprisingly expensive. Clamp renderer pixel ratio with `Math.min(window.devicePixelRatio, 2)` or even a lower value for heavy scenes. The difference is often invisible to users but very visible to battery and frame rate.

For static product viewers, sharpness matters. For animated shader backgrounds, smoothness may matter more. Make the pixel ratio choice per scene instead of copying one value across every project.

Resize when the container changes

Window resize is a start, but modern layouts can change canvas size without a full window resize. Sidebars open, tabs change, browser UI collapses, and CSS grid tracks shift. ResizeObserver is a better fit for tool panels and responsive hero scenes because it watches the actual container.

The render loop can call a cheap resize check before drawing, but do not trigger expensive work if width and height did not change. Compare the current renderer size to the target size, then update only when needed.

Test the awkward sizes

Responsive scenes often fail between common breakpoints. Test a narrow phone, a tablet width, and a very wide desktop. Check whether text overlays still have contrast, whether the object remains framed, and whether UI controls overlap the canvas. A technically responsive canvas can still produce a poor composition.

For AdSense-friendly pages, this testing also matters because layout quality affects perceived trust. A guide page with a broken hero canvas on mobile feels unfinished even if the article text is strong.

Resize a live canvas and inspect the result

The examples gallery uses container dimensions for its canvases and keeps the surrounding page readable at desktop and mobile widths.

  • Canvas follows its container
  • Camera aspect updates
  • Page remains usable without the preview
Open the responsive examples

Compare CSS pixels before resizing the buffer

Assigning the window size on every frame wastes work and ignores embedded containers.

Fragile pattern
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
Testable pattern
const width = container.clientWidth;
const height = container.clientHeight;
if (canvas.width !== width * pixelRatio || canvas.height !== height * pixelRatio) {
  renderer.setSize(width, height, false);
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
}

Viewport check matrix

These are the two automated viewport sizes used by the local smoke checks.

ViewportExpected condition
390 x 844No horizontal document overflow
1366 x 900Canvas at least 120 x 120 CSS pixels
Pixel ratioClamped to at most 2
Reduced motionStatic first frame remains visible

Reproduce a blurry or stretched canvas

Separate layout size, drawing-buffer size, and camera projection while debugging.

  1. Record the canvas CSS width and height.
  2. Record canvas.width and canvas.height after pixel-ratio scaling.
  3. Confirm camera.aspect uses the same CSS dimensions.
  4. Resize the container without reloading and repeat the measurements.

Resize helper

function resizeRenderer(renderer, camera, container) {
  const width = Math.max(1, container.clientWidth);
  const height = Math.max(1, container.clientHeight);
  const current = renderer.getSize(new THREE.Vector2());
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));

  if (current.x !== width || current.y !== height) {
    renderer.setSize(width, height, false);
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
  }
}

Sources and further reading

Update record

  • : Rewritten around a reproducible demo, explicit test record, code comparison, and known platform limits.

Related guides