TL;DR:

  • WebXR Device API is a W3C standard that gives browsers access to XR hardware — enabling immersive AR and VR experiences without native app installation
  • Supported on Meta Quest (native Chromium), Magic Leap, HTC Vive via SteamVR’s browser, and on Android devices for AR through WebXR AR module; Apple’s Safari supports basic WebXR but limits AR features
  • Frameworks like Three.js, Babylon.js, A-Frame, and PlayCanvas abstract the raw API into practical development workflows
  • Best suited for: product visualisation, training simulations, XR-enabled web content, demos, and scenarios where distribution friction matters; native apps remain better for graphics-intensive or hardware-deep use cases

Native XR apps have real advantages: lower latency, deeper hardware integration, better performance ceilings, more reliable certification for enterprise deployment. But they also require native SDK expertise, platform-specific builds, App Store submission (or MDM distribution for enterprise), and update cycles that go through platform review.

WebXR carves out a different space. An immersive experience delivered as a URL — scannable from a QR code, shareable as a link, accessible from a browser on a headset without any installation step — solves a different class of problem. The distribution and access model is closer to a web page than a native app, with corresponding deployment benefits and performance tradeoffs.

If you’re building product visualisation tools for e-commerce, training simulations where distribution to a varied device fleet matters, or prototyping XR concepts before committing to native development, WebXR is worth understanding as a first-class option rather than a fallback.

How WebXR Works

The WebXR Device API exposes XR hardware capabilities through JavaScript. A WebXR session is a rendering loop that receives pose data (head, hand, controller positions) and renders frames to the XR display.

The lifecycle of a WebXR application:

  1. Check support: Query navigator.xr and call navigator.xr.isSessionSupported() for the session mode you want
  2. Request session: The user gesture initiates the XR session — this is required to prevent autoplay XR
  3. Enter the render loop: Request animation frames, receive pose data each frame, render
  4. Handle session end: The user exits XR, clean up
// Check if WebXR is available and immersive VR is supported
async function checkSupport() {
  if (!navigator.xr) {
    console.log("WebXR not supported");
    return false;
  }

  const supported = await navigator.xr.isSessionSupported("immersive-vr");
  return supported;
}

// Start a VR session (must be triggered by user gesture)
async function enterVR() {
  try {
    const session = await navigator.xr.requestSession("immersive-vr", {
      requiredFeatures: ["local-floor"],
      optionalFeatures: ["hand-tracking"],
    });
    
    startRenderLoop(session);
  } catch (err) {
    console.error("Failed to start XR session:", err);
  }
}

Session Modes

WebXR supports three session modes:

inline: Non-immersive, renders in the page canvas. Useful for 3D viewer previews before entering full immersion.

immersive-vr: Full virtual reality — replaces the user’s view entirely. Requires a headset.

immersive-ar: Augmented reality — overlays digital content on the real world. Requires a device that supports see-through AR (Android with ARCore on mobile, or AR headsets).

// Request AR session with hit testing for surface detection
const session = await navigator.xr.requestSession("immersive-ar", {
  requiredFeatures: ["hit-test"],
  optionalFeatures: ["dom-overlay"],
  domOverlay: { root: document.getElementById("ar-overlay") }
});

The Render Loop

Each frame, WebXR provides a XRFrame containing the current pose data. You extract the viewer pose (where the headset is looking), get render state, and draw your scene.

function startRenderLoop(session) {
  const gl = canvas.getContext("webgl2", { xrCompatible: true });
  
  session.updateRenderState({
    baseLayer: new XRWebGLLayer(session, gl)
  });

  const referenceSpace = await session.requestReferenceSpace("local-floor");

  function onXRFrame(time, frame) {
    session.requestAnimationFrame(onXRFrame);
    
    const pose = frame.getViewerPose(referenceSpace);
    if (!pose) return;

    const glLayer = session.renderState.baseLayer;
    gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
    gl.clearColor(0, 0, 0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // Render for each eye (views array)
    for (const view of pose.views) {
      const viewport = glLayer.getViewport(view);
      gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
      
      // Use view.projectionMatrix and view.transform for rendering
      renderScene(gl, view.projectionMatrix, view.transform.inverse.matrix);
    }
  }

  session.requestAnimationFrame(onXRFrame);
}

Working directly with the WebXR API and WebGL2 gives maximum control but requires significant graphics programming knowledge. In practice, almost all WebXR development uses a framework that wraps this loop.

Frameworks: Where You’ll Actually Spend Time

Three.js is the most widely used JavaScript 3D library and has WebXR support built in. The WebXRManager handles session lifecycle, the render loop, and controller input.

import * as THREE from 'three';
import { VRButton } from 'three/addons/webxr/VRButton.js';

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.xr.enabled = true;

// Add the VR entry button to the DOM
document.body.appendChild(VRButton.createButton(renderer));

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);

// Standard Three.js scene setup
scene.add(new THREE.DirectionalLight(0xffffff, 1));
scene.add(new THREE.AmbientLight(0x404040));

// Add objects to scene as normal
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material = new THREE.MeshStandardMaterial({ color: 0x4488ff });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 1.5, -2);
scene.add(cube);

// Render loop — Three.js handles XR frame loop when xr.enabled is true
renderer.setAnimationLoop(() => {
  renderer.render(scene, camera);
});

Babylon.js is Microsoft-backed and stronger in enterprise contexts, particularly for Windows Mixed Reality and enterprise XR deployments. It has built-in XR experience helpers that abstract session management further.

import * as BABYLON from "@babylonjs/core";

const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);

// XR Experience helper handles all WebXR session management
const xrHelper = await scene.createDefaultXRExperienceAsync({
  uiOptions: {
    sessionMode: "immersive-vr",
  },
  optionalFeatures: true,
});

// Add your scene objects
const box = BABYLON.MeshBuilder.CreateBox("box", { size: 0.5 }, scene);
box.position = new BABYLON.Vector3(0, 1.5, -2);

A-Frame is HTML-based and requires the least JavaScript for basic scenes:

<!DOCTYPE html>
<html>
  <head>
    <script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
  </head>
  <body>
    <a-scene>
      <a-box position="0 1.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
      <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
      <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
      <a-sky color="#ECECEC"></a-sky>
    </a-scene>
  </body>
</html>

A-Frame scenes work in VR automatically on compatible devices — the VR entry button appears without any additional code.

AR Hit Testing

One of WebXR’s most practically useful features for AR is hit testing — detecting where a virtual object would land on a real-world surface (floor, table, wall) as the user moves their device.

// Request AR session with hit testing
const session = await navigator.xr.requestSession("immersive-ar", {
  requiredFeatures: ["hit-test"],
});

const referenceSpace = await session.requestReferenceSpace("viewer");
const hitTestSource = await session.requestHitTestSource({
  space: referenceSpace,
});

// In the render loop:
function onFrame(time, frame) {
  session.requestAnimationFrame(onFrame);
  
  const hitTestResults = frame.getHitTestResults(hitTestSource);
  
  if (hitTestResults.length > 0) {
    const hit = hitTestResults[0];
    const pose = hit.getPose(localReferenceSpace);
    
    // Place a reticle or object at the hit position
    reticle.visible = true;
    reticle.matrix.fromArray(pose.transform.matrix);
  } else {
    reticle.visible = false;
  }
}

This is the foundation for product placement AR — furniture in a room, equipment in a workspace, signage in a retail environment. The hit test gives you a surface normal and position where you can anchor a virtual object.

Hand Tracking

WebXR supports hand tracking on devices that expose it (Meta Quest, newer Android ARCore devices):

const session = await navigator.xr.requestSession("immersive-vr", {
  optionalFeatures: ["hand-tracking"],
});

// In the frame loop:
for (const inputSource of session.inputSources) {
  if (inputSource.hand) {
    const wrist = inputSource.hand.get("wrist");
    const wristPose = frame.getJointPose(wrist, referenceSpace);
    
    if (wristPose) {
      // wristPose.transform contains position and orientation
      // Update hand model or interaction logic
    }
    
    // Access individual finger joints
    const indexTip = inputSource.hand.get("index-finger-tip");
    const thumbTip = inputSource.hand.get("thumb-tip");
    // Pinch detection: check distance between index tip and thumb tip
  }
}

Hand tracking in WebXR is available on Meta Quest browsers as of Quest firmware 50+ and works with the same API on desktop browsers connected to hand-tracking-capable devices.

Browser Support Reality Check (2026)

Meta Quest Browser (Chromium): Full WebXR support including VR, AR (passthrough), hand tracking, hit testing. The most capable WebXR runtime for headsets.

Chrome (Android with ARCore): Full immersive-ar support including hit testing, light estimation. Works on ARCore-compatible Android phones.

Chrome (desktop/Windows): VR support via OpenXR (SteamVR, WMR). AR not available on desktop.

Firefox Reality / Wolvic: VR support on Quest, modest adoption.

Safari (iOS/visionOS): WebXR is partially supported in Safari 17+, with significant limitations on AR features. Apple’s visionOS Safari does support basic immersive-vr sessions, but the platform’s preferred XR approach is native RealityKit. Expect a gap between WebXR capability on Quest and on Apple hardware.

Samsung Internet (Galaxy headsets): WebXR support aligned with Chromium, good compatibility with Android XR experiences.

Practical Deployment Considerations

HTTPS is required: WebXR sessions only work on secure origins. Your development server needs HTTPS (use a self-signed cert for local dev) and production requires a valid certificate.

Performance budgets: WebXR rendering on mobile hardware (Quest 3, Android phones) has strict performance requirements. Aim for 72fps on Quest (72Hz refresh) and budget draw calls carefully. Three.js’s instancing API and Babylon.js’s batching help significantly for scenes with many repeated objects.

Progressive enhancement: Build your XR experience with a standard 2D fallback. Use isSessionSupported() to detect WebXR capability and show appropriate UI — “View in 3D” for non-XR devices, “Enter AR” on AR-capable devices, “Enter VR” on headsets.

Loading times matter more in XR: Users in a headset can’t easily read a loading screen or alt-tab while assets load. Pre-load assets before requesting the XR session so the experience is immediate when it starts.

WebXR’s distribution advantages are real. An AR product viewer that’s a link in an email, a VR training scenario that IT can deploy without App Store involvement, a spatial prototype that stakeholders can experience from a QR code — these use cases don’t require the full native development investment to deliver meaningful value. The tooling has matured enough in 2026 that Three.js or Babylon.js WebXR projects can be production-quality for a significant portion of business XR use cases.