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.