Create WebXR DOM Overlays in Three.js

This article explains how to build an interactive HTML/DOM overlay that floats seamlessly over a WebXR augmented reality (AR) camera feed using Three.js. You will learn how to configure the WebXR session, structure the HTML and CSS for proper layering, and handle user interactions between standard web elements and the 3D WebXR environment.

1. Define the HTML Structure

To create a DOM overlay, you need a container element in your HTML that holds all the UI elements (buttons, text, menus) you want to display over the AR view.

<div id="overlay-container">
  <div id="ui-screen">
    <h1>AR Dashboard</h1>
    <button id="action-button">Spawn Object</button>
  </div>
</div>

2. Style the Overlay with CSS

For the overlay to look correct and allow interactions to pass through to the 3D scene, you must position it absolutely and manage the pointer-events property.

#overlay-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: none; /* Hidden until the AR session starts */
  pointer-events: none; /* Allows taps to pass through to the 3D scene */
  z-index: 10;
}

#ui-screen {
  padding: 20px;
}

#action-button {
  pointer-events: auto; /* Re-enables interaction specifically for the button */
  padding: 12px 24px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
}

Setting pointer-events: none on the root container ensures that users can still tap on the physical floor or objects in AR to perform hit-testing. Setting pointer-events: auto on the button ensures the button remains clickable.

3. Configure the WebXR Session in Three.js

When initializing the WebXR session using Three.js’s ARButton, you must request the dom-overlay feature and specify the root DOM element you want to overlay.

import { ARButton } from 'three/examples/jsm/webxr/ARButton.js';

// Setup your renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);

const overlayContainer = document.getElementById('overlay-container');

// Configure WebXR options
const sessionInit = {
  requiredFeatures: ['hit-test'],
  optionalFeatures: ['dom-overlay'],
  domOverlay: { root: overlayContainer }
};

// Create and add the AR Button to the page
const arButton = ARButton.createButton(renderer, sessionInit);
document.body.appendChild(arButton);

The WebXR browser agent will automatically toggle the visibility of the overlayContainer (changing its display state) when the AR session starts and ends.

4. Handle Interaction Events

Once the overlay is active, you can attach standard JavaScript event listeners to your DOM elements. These listeners can directly manipulate your Three.js scene.

const actionButton = document.getElementById('action-button');

actionButton.addEventListener('click', (event) => {
  // Prevent WebXR select events from firing simultaneously
  event.stopPropagation(); 
  
  // Trigger your Three.js logic here
  spawnVirtualObject();
});

function spawnVirtualObject() {
  const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
  const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
  const cube = new THREE.Mesh(geometry, material);
  
  // Position the cube in front of the camera
  cube.position.set(0, 0, -0.5).applyMatrix4(camera.matrixWorld);
  scene.add(cube);
}

Using event.stopPropagation() is critical. It prevents the click event on the HTML button from bubbling down and triggering a WebXR select event (which would otherwise register as a tap in the 3D space).