Programmatically Enter WebXR in Three.js with Custom UI

This article explains how to bypass the default, pre-styled VRButton provided by Three.js and programmatically trigger a WebXR immersive session using your own custom user interface. By leveraging the browser’s native WebXR Device API alongside Three.js’s WebGLRenderer, you can maintain complete control over the design, placement, and behavior of your virtual reality launch buttons.

To programmatically enter a WebXR session with a custom UI, you must bypass the standard VRButton helper entirely and interact directly with navigator.xr.

Because browser security policies require a user gesture (such as a click) to enter immersive VR, you must bind the session request directly to your custom button’s click event.

Step 1: Create Your Custom HTML Button

First, define your custom button in your HTML document. You can style this button using standard CSS to fit your application’s design.

<button id="custom-vr-button" disabled>Loading VR...</button>

Step 2: Check for WebXR Support

In your JavaScript file, check if the user’s browser and hardware support WebXR. If supported, enable your custom button.

import * as THREE from 'three';

const button = document.getElementById('custom-vr-button');
let currentSession = null;

// Initialize your Three.js renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.xr.enabled = true; // Crucial for Three.js to process XR

if (navigator.xr) {
  navigator.xr.isSessionSupported('immersive-vr')
    .then((supported) => {
      if (supported) {
        button.textContent = 'Enter VR';
        button.disabled = false;
        button.addEventListener('click', handleVRButtonClick);
      } else {
        button.textContent = 'VR Not Supported';
      }
    })
    .catch((err) => {
      console.error('Error checking WebXR support:', err);
      button.textContent = 'VR Error';
    });
} else {
  button.textContent = 'WebXR Not Available';
}

Step 3: Request and Bind the WebXR Session

When the user clicks your custom button, request the WebXR session and pass it to Three.js using renderer.xr.setSession().

function handleVRButtonClick() {
  if (currentSession === null) {
    // Request a new session
    navigator.xr.requestSession('immersive-vr', {
      optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking']
    })
    .then(onSessionStarted)
    .catch((err) => {
      console.error('Failed to start WebXR session:', err);
    });
  } else {
    // End the active session
    currentSession.end();
  }
}

function onSessionStarted(session) {
  currentSession = session;
  button.textContent = 'Exit VR';

  // Attach the session to Three.js
  renderer.xr.setSession(session);

  // Listen for when the session ends (either programmatically or by the system)
  session.addEventListener('end', onSessionEnded);
}

function onSessionEnded() {
  currentSession = null;
  button.textContent = 'Enter VR';
  
  // Three.js handles internal cleanup automatically when the session ends
}

By manually calling navigator.xr.requestSession and passing the resolved session to renderer.xr.setSession(session), you gain the freedom to build complex, responsive UI overlays, loading screens, and custom entry flows while keeping Three.js in sync with the XR hardware state.