Overlay HTML Elements on a WebGL Canvas

Overlaying standard HTML DOM elements over a WebGL canvas allows developers to create rich user interfaces, interactive annotations, and accessible text layers directly integrated with 3D scenes. This guide covers the essential CSS layout techniques for stacking, managing user input across layers using pointer events, synchronizing 3D object coordinates to 2D screen positions, and optimizing rendering performance.

CSS Layering and Container Setup

The foundation of overlaying HTML on a WebGL canvas relies on CSS absolute positioning within a shared relative container. By placing both the <canvas> element and an overlay container inside a wrapper, you ensure they scale and align identically.

<div class="webgl-container">
  <canvas id="webgl-canvas"></canvas>
  <div class="ui-overlay">
    <button class="interactive-btn">Click Me</button>
    <div class="annotation" id="label">3D Target</div>
  </div>
</div>
.webgl-container {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

#webgl-canvas,
.ui-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.ui-overlay {
  pointer-events: none; /* Allows mouse events to pass through to the canvas */
}

.ui-overlay .interactive-btn {
  pointer-events: auto; /* Re-enables clicks on specific UI elements */
}

Managing Pointer Events

By default, an HTML element layered over a canvas intercepts all mouse and touch interactions, preventing users from rotating, panning, or interacting with the 3D scene.

Setting pointer-events: none; on the .ui-overlay container passes all clicks, scrolls, and drag gestures straight through to the underlying WebGL canvas. For elements within the overlay that require user interaction (such as buttons, links, or text inputs), explicitly declare pointer-events: auto;.

Synchronizing 3D Positions to 2D Screen Space

To anchor an HTML element to a specific 3D coordinate or mesh, you must convert the object's 3D world space coordinates to 2D Normalized Device Coordinates (NDC), and then to screen pixel coordinates.

Using a framework like Three.js, this projection is handled as follows:

function updateOverlayPosition(object3D, domElement, camera, renderer) {
  const vector = new THREE.Vector3();
  
  // Get the world position of the 3D object
  object3D.getWorldPosition(vector);
  
  // Project coordinates to Normalized Device Coordinates (-1 to +1)
  vector.project(camera);
  
  // Check if the object is behind the camera
  if (vector.z > 1) {
    domElement.style.display = 'none';
    return;
  }
  
  domElement.style.display = 'block';

  // Convert NDC to CSS pixel coordinates
  const canvas = renderer.domElement;
  const x = (vector.x * 0.5 + 0.5) * canvas.clientWidth;
  const y = (-(vector.y * 0.5) + 0.5) * canvas.clientHeight;

  // Position the element using hardware-accelerated transforms
  domElement.style.transform = `translate(-50%, -50%) translate3d(${x}px, ${y}px, 0)`;
}

Call this update function inside your WebGL render loop (requestAnimationFrame) to ensure the DOM element tracks the 3D object smoothly during camera movement or object transformations.

Performance Optimization

  1. Use transform Instead of top and left: Animating top and left triggers browser layout recalculations and repaints on every frame. Using transform: translate3d() leverages the GPU for compositing, ensuring 60+ FPS performance.
  2. Handle Occlusion and Frustum Culling: Elements positioned behind the camera (vector.z > 1) or behind other 3D geometry should have their display property set to none or visibility set to hidden to reduce unnecessary compositing overhead.
  3. Limit DOM Complexity: Avoid placing thousands of individual DOM elements over a canvas. If hundreds of dynamic labels are required, render them inside the WebGL canvas using texture atlases or instanced sprite rendering, reserving HTML overlays strictly for elements requiring complex HTML form controls, accessibility features, or native text selection.