When you need to show measurements, labels, annotations, icons or temporary drawings over a Three.js model — especially in architecture, where placement and numerical accuracy matter — you have several practical options. Below are the commonly used techniques with short code snippets and a comparison, so you can pick the right tool for each job.
1) Canvas 2D overlay (project 3D → 2D and draw on an HTML canvas)
You keep a normal WebGL canvas for Three.js and place a transparent HTML <canvas> on top (position: absolute). Each animation frame you project 3D points with vector.project(camera) and draw shapes and text at the projected 2D positions with the Canvas 2D API.
<canvas id="webglCanvas"></canvas>
<canvas id="overlayCanvas" style="position:absolute; top:0; left:0; pointer-events:none;"></canvas>
<script type="module">
import * as THREE from "three";
const overlayCanvas = document.getElementById("overlayCanvas");
const overlayCtx = overlayCanvas.getContext("2d");
// ... scene, camera, renderer and controls ...
overlayCanvas.width = window.innerWidth;
overlayCanvas.height = window.innerHeight;
const points = [new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 1, 0)];
const tempPoint = new THREE.Vector3(1, 0.5, 0);
function projectTo2D(vec3) {
const v = vec3.clone().project(camera);
return {
x: (v.x * 0.5 + 0.5) * overlayCanvas.width,
y: (-v.y * 0.5 + 0.5) * overlayCanvas.height,
};
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
// Clear and redraw the overlay every frame
overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
overlayCtx.fillStyle = "red";
points.forEach(p => {
const { x, y } = projectTo2D(p);
overlayCtx.beginPath();
overlayCtx.arc(x, y, 5, 0, Math.PI * 2);
overlayCtx.fill();
});
const start = projectTo2D(points[0]);
const end = projectTo2D(tempPoint);
overlayCtx.strokeStyle = "yellow";
overlayCtx.lineWidth = 2;
overlayCtx.beginPath();
overlayCtx.moveTo(start.x, start.y);
overlayCtx.lineTo(end.x, end.y);
overlayCtx.stroke();
}
animate();
</script>When to use: quick previews and transient visuals (dragging a new line while the user moves the mouse), many items where HTML/CSS styling is not required and GPU meshes would be heavy, and when you need maximum 2D drawing flexibility.
Pros: very fast to implement; the Canvas 2D API is simple and flexible; good for temporary visuals and many dynamic overlays.
Cons: it is not a real 3D object — you must re-project and redraw every frame to stay in sync with the camera, or the overlay drifts. Text styling is limited compared to HTML/CSS, and coordinates must be scaled with devicePixelRatio for crisp lines.
2) Three.js 3D geometry (Line, LineSegments, Points, small meshes)
Create actual 3D geometry — THREE.Line with a BufferGeometry for a measurement segment, THREE.SphereGeometry or THREE.Points for markers. These objects live in the scene graph and are rendered by WebGL, so they naturally obey camera transforms, occlusion and spatial relationships.
// A persistent 3D line between two Vector3s (a, b)
const geometry = new THREE.BufferGeometry().setFromPoints([a, b]);
const material = new THREE.LineBasicMaterial({ color: 0xffaa00, linewidth: 2 });
const line = new THREE.Line(geometry, material);
scene.add(line);When to use: persistent measurements that must snap to geometry and stay exact as the camera moves; when occlusion or depth relationships with other meshes matter; when you need editing, selection or snapping on the object itself.
Pros: the most accurate and robust option — lines and pins are real 3D objects, naturally synchronized with the camera. You can use GPU performance (BufferGeometry) and shaders to style large numbers of lines.
Cons: slightly more complex (you manage geometries, updates and memory). For thousands of small meshes, batch them or use instancing to keep performance good.
3) Sprites (THREE.Sprite) and textured quads
THREE.Sprite is a plane that always faces the camera and is textured with a bitmap — good for small icons or marker images. Sprites live in 3D, so they follow transforms and depth, but they are raster-based.
const labelCanvas = document.createElement("canvas");
// draw text into the canvas context...
const texture = new THREE.CanvasTexture(labelCanvas);
const spriteMaterial = new THREE.SpriteMaterial({ map: texture, depthTest: false });
const sprite = new THREE.Sprite(spriteMaterial);
sprite.position.copy(point3D);
scene.add(sprite);When to use: small icons or labels that should appear in 3D and always face the camera, or text rendered as a texture.
Pros: simple and in-scene (follows the camera and occlusion); can be generated from a canvas dynamically.
Cons: each sprite uses a GPU texture, so many sprites need batching or instancing. Text quality depends on resolution — scaling can blur it.
4) CSS2DRenderer (HTML labels attached to 3D positions)
Three.js CSS2DRenderer attaches DOM elements to CSS2DObjects in the scene. The elements are moved to the correct screen position every frame, so they appear attached to 3D points. They remain DOM, so you can style them with CSS and add events and tooltips.
import { CSS2DRenderer, CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer.js";
const label = document.createElement("div");
label.className = "label";
label.innerText = "3.24 m";
const labelObject = new CSS2DObject(label);
labelObject.position.copy(point3D);
scene.add(labelObject);
// In the render loop, alongside the WebGL renderer
labelRenderer.render(scene, camera);
renderer.render(scene, camera);When to use: styled text labels, interactive HTML tooltips and clickable annotation UIs — whenever you need full CSS styling.
Pros: crisp text and easy styling with HTML/CSS; great for labels and interactive UI.
Cons: DOM elements are not WebGL, so performance degrades with hundreds of labels. For typical architectural use (tens to low hundreds) it is very practical.
5) CSS3DRenderer (real DOM in 3D transforms)
CSS3DRenderer maps DOM elements into 3D transforms, so you can place HTML in true 3D with rotation and scale. It is useful for interactive panels, embedded web pages and UI elements that must take part in 3D transforms, such as a rotating billboard with HTML content. It is powerful for specific UX, but heavier and more complex; DOM count and CSS transforms are the main limits.
6) The hybrid approach (recommended for architectural measurement tools)
A practical, robust pattern:
- Use 3D lines and points (real
THREE.Line/THREE.Meshspheres) for persistent measurements — this guarantees accuracy, selection, snapping and correct depth. - Use CSS2DRenderer for labels (numbers, units, interactive HTML) for crisp fonts and CSS styling.
- Use a Canvas 2D overlay only for temporary previews, such as dragging a new measurement — cheap and instant.
- Optionally use sprites for many tiny icons, if you batch or instance them carefully.
This hybrid gives you the best of accuracy, UI flexibility and performance.



