Three.js UI and HUD: In-Scene vs HTML Overlay

Most Three.js UI and HUD elements (score displays, menus, tooltips, labels) are built as regular HTML/CSS positioned over the WebGL canvas, not as 3D geometry, because HTML gives you real accessibility, text selection, CSS styling and browser-native interaction for free. True in-scene 3D UI (geometry that exists inside the 3D world) is reserved for elements that specifically need to be part of the 3D scene itself, like a control panel mounted on an in-scene object.

Last updated . Verified against three.js r181.

Read the FAQJump to code

The HTML overlay approach (the default choice)

A <canvas> element sits in normal HTML document flow, which means regular HTML/CSS can be positioned directly on top of it with standard position: absolute layout, with no special integration needed for static UI (a fixed HUD frame, menu, buttons). For UI elements that need to track a moving 3D object (a health bar over a character's head, a label following a data point), project the object's world position to 2D screen coordinates via Vector3.project(camera), then position the HTML element with the resulting normalized coordinates converted to pixels.

React Three Fiber's drei library ships <Html> for exactly this pattern if you're in a React project. It handles the projection math for you.

When true in-scene UI makes sense

Reach for actual 3D geometry as UI specifically when the UI needs to be part of the 3D world: a control panel physically mounted on a spaceship's dashboard in a VR cockpit, a floating menu a user reaches out and touches in WebXR, or stylized diegetic UI that's meant to look like it exists in the scene rather than as an overlay. This is built from meshes with text (troika-three-text or TextGeometry) and raycasting for click/touch interaction, since there's no DOM click event for a 3D mesh. You detect interaction with a raycast, not a browser event.

WebXR specifically often requires true in-scene UI, since an HTML overlay doesn't exist "in" the immersive session at all. The headset only sees what's rendered into the 3D scene.

Performance and z-index considerations

HTML overlay elements that update every frame (a position-tracking label) can cause layout thrashing if not handled carefully. Batch DOM updates, and prefer transform: translate() over changing top/left for per-frame position updates, since transform avoids triggering a full browser layout recalculation. For elements that should be occluded by 3D geometry in front of them (a label behind an object shouldn't show through), you either need true in-scene UI (which the depth buffer naturally occludes) or manual raycasting to check visibility before showing the HTML element. An HTML overlay has no inherent awareness of the 3D scene's depth.

Code

World-to-screen projection for an HTML label (r181)
1function updateLabelPosition(object3D, labelEl, camera, renderer) {
2 const vector = object3D.position.clone().project(camera)
3 const halfWidth = renderer.domElement.clientWidth / 2
4 const halfHeight = renderer.domElement.clientHeight / 2
5 const x = vector.x * halfWidth + halfWidth
6 const y = -vector.y * halfHeight + halfHeight
7 labelEl.style.transform = `translate(${x}px, ${y}px)` // avoids layout thrash
8 labelEl.style.display = vector.z < 1 ? 'block' : 'none' // hide when behind the camera
9}
React Three Fiber + drei <Html> (equivalent)
1import { Html } from '@react-three/drei'
2
3function HealthBar({ position, value }) {
4 return (
5 <Html position={position} center distanceFactor={8}>
6 <div className="health-bar">{value}%</div>
7 </Html>
8 )
9}
10// drei handles the world-to-screen projection and re-renders automatically.

Learn this properly

Learn Practical TSL

Your First Node Material

Build the 3D fundamentals first, then layer UI on top.

Start the lesson (6 minutes)

Frequently asked questions

Should Three.js UI be built with HTML or 3D geometry?

HTML overlay for almost everything: it's cheaper, accessible, and gets real browser interaction for free. Reserve true 3D-geometry UI for cases that specifically need to exist inside the 3D world, like WebXR controls or diegetic in-scene panels.

How do I position an HTML element over a moving 3D object in Three.js?

Project the object's world position to normalized device coordinates with Vector3.project(camera), convert that to pixel coordinates based on your canvas size, and position the HTML element with those pixels. Update every frame the object moves.

Can HTML UI be occluded by 3D objects in Three.js?

Not automatically. An HTML overlay has no awareness of the 3D scene's depth buffer. You need either true in-scene 3D UI (which is naturally depth-tested) or manual raycasting to detect when the tracked object is actually visible before showing the label.

Keep reading