Overlay HTML Labels on Three.js 3D Objects

This article explains how to use the CSS2DRenderer in Three.js to overlay standard HTML elements as labels onto 3D objects. You will learn what the CSS2DRenderer is, why it is a powerful tool for web developers, and the step-by-step implementation process to make 2D HTML elements seamlessly track 3D coordinates in your WebGL scene.

What is CSS2DRenderer?

The CSS2DRenderer is an official Three.js add-on that allows you to combine standard HTML/CSS elements with a 3D WebGL scene.

By default, rendering crisp text in WebGL requires complex techniques like canvas textures or Signed Distance Field (SDF) fonts. The CSS2DRenderer bypasses this complexity by rendering your HTML elements in a separate DOM layer positioned directly above the WebGL canvas. It translates the 3D coordinates of your objects into 2D screen coordinates, allowing HTML elements to follow 3D meshes as the camera moves, rotates, and zooms.

Step-by-Step Implementation

To overlay HTML labels onto 3D objects, you need to set up the CSS2DRenderer alongside your standard WebGL renderer.

1. Import the Required Modules

First, import the renderer and the 2D object wrapper from the Three.js library:

import * as THREE from 'three';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';

2. Set Up the CSS2DRenderer

Create an instance of the CSS2DRenderer, style its DOM element to overlay perfectly on top of your WebGL canvas, and append it to the document body.

// Initialize WebGL Renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Initialize CSS2D Renderer
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.pointerEvents = 'none'; // Allows clicking through labels to control the 3D scene
document.body.appendChild(labelRenderer.domElement);

3. Create the HTML Label and Wrap It

Create a standard HTML element using vanilla JavaScript, style it with CSS, and wrap it in a CSS2DObject.

// Create HTML element
const labelDiv = document.createElement('div');
labelDiv.className = 'label';
labelDiv.textContent = 'Earth';
labelDiv.style.color = '#ffffff';
labelDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
labelDiv.style.padding = '5px 10px';
labelDiv.style.borderRadius = '5px';
labelDiv.style.fontFamily = 'sans-serif';

// Wrap the element in a CSS2DObject
const labelObject = new CSS2DObject(labelDiv);
labelObject.position.set(0, 1.5, 0); // Position the label slightly above the object's center

4. Attach the Label to a 3D Object

Add the CSS2DObject as a child of the target 3D mesh. Because it is a child of the mesh, it will automatically move and rotate with the mesh.

// Create a 3D object (e.g., a sphere)
const geometry = new THREE.SphereGeometry(1, 32, 32);
const material = new THREE.MeshBasicMaterial({ color: 0x0000ff });
const earthMesh = new THREE.Mesh(geometry, material);
scene.add(earthMesh);

// Attach the label to the mesh
earthMesh.add(labelObject);

5. Render Both Scenes in the Animation Loop

In your animation loop, you must render both the standard WebGL scene and the CSS2D scene.

function animate() {
    requestAnimationFrame(animate);

    // Rotate mesh to demonstrate the label tracking
    earthMesh.rotation.y += 0.01;

    // Render WebGL
    renderer.render(scene, camera);

    // Render HTML Labels
    labelRenderer.render(scene, camera);
}

animate();

6. Handle Window Resizing

Ensure both renderers resize correctly when the browser window dimensions change.

window.addEventListener('resize', onWindowResize);

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);
    labelRenderer.setSize(window.innerWidth, window.innerHeight);
}