Three.js is the most widely used WebGL library on the web, and the layer underneath much of the WebXR ecosystem — A-Frame wraps it, and a large share of the WebXR samples are written directly against it. Where A-Frame is declarative and Babylon.js is a batteries-included engine, three.js sits in between: a lean, imperative toolkit that gives full control of the scene graph while keeping the WebXR plumbing down to a couple of calls.

threejs.org · docs · WebXR examples · GitHub (MIT, r170, 105k★)

What three.js adds for XR

The whole of WebXR support lives in renderer.xr — three.js’s WebXRManager. Turning it on is one property; entering a session is one button helper:

  • renderer.xr.enabled = true routes rendering through the headset’s stereo views and per-eye projection.
  • VRButton / ARButton (in three/addons/webxr/) handle navigator.xr.requestSession, feature negotiation, and the enter/exit UI.
  • renderer.setAnimationLoop(fn) replaces requestAnimationFrame — it is XR-aware and ticks once per eye-pair at the headset’s frame rate.
  • renderer.xr.getController(i) and XRHandModelFactory surface controllers and articulated hands as ordinary Object3Ds in the scene.

The starter — one HTML file, no build step

Modern three.js ships as ES modules. An import map pulls the library from a CDN, so the entire starter is a single file with no bundler:

<!DOCTYPE html>
<html><body style="margin:0">
<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
} }
</script>
<script type="module">
  import * as THREE from 'three';
  import { VRButton } from 'three/addons/webxr/VRButton.js';

  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(innerWidth, innerHeight);
  renderer.xr.enabled = true;                          // ① enable WebXR
  document.body.appendChild(renderer.domElement);
  document.body.appendChild(VRButton.createButton(renderer)); // ② Enter VR

  const scene  = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(70, innerWidth / innerHeight, 0.1, 100);
  scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 3));

  const cube = new THREE.Mesh(
    new THREE.BoxGeometry(0.4, 0.4, 0.4),
    new THREE.MeshStandardMaterial({ color: 0x23f0ff }));
  cube.position.set(0, 1.5, -2);
  scene.add(cube);

  renderer.setAnimationLoop(() => {                    // ③ XR-aware render loop
    cube.rotation.y += 0.01;
    renderer.render(scene, camera);
  });
  addEventListener('resize', () => {
    camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
  });
</script>
</body></html>

Those three marked lines are the entire difference between a desktop WebGL page and a headset app. Swap VRButton for ARButtonARButton.createButton(renderer, { requiredFeatures: ['hit-test'] }) — to start an immersive-ar passthrough session instead.

Controllers and hands

Both come out of the same manager. Add a controller as a scene object and listen for its select events; load hand meshes through the factory:

import { XRHandModelFactory } from 'three/addons/webxr/XRHandModelFactory.js';

const controller = renderer.xr.getController(0);
controller.addEventListener('select', () => { /* trigger pull */ });
scene.add(controller);

const hands = new XRHandModelFactory();
const hand = renderer.xr.getHand(0);
hand.add(hands.createHandModel(hand, 'mesh'));
scene.add(hand);

For grips and ray pointers, renderer.xr.getControllerGrip(i) plus XRControllerModelFactory renders the physical controller model; the webxr_vr_dragging and webxr_xr_ballshooter examples are the canonical references.

Deploy & target devices

WebXR requires HTTPS. The same static hosts and the cloudflared quick-tunnel one-liner from the A-Frame article apply unchanged:

cloudflared tunnel --url http://localhost:8080

Headsets available at the event, and how three.js reaches them:

  • Meta Quest 3 — Meta Quest Browser: full WebXR — immersive-vr, immersive-ar passthrough, hand tracking, AR features. The smoothest target.
  • PICO 4 Ultra Enterprise — the PICO Browser exposes WebXR on the same standalone Android base; several units are on hand at the hackathon. Test the Enter VR flow early, as on any standalone.
  • Apple Vision Pro — Safari on visionOS: immersive-vr with hand and transient-pointer input (no WebXR AR module). Keep interactions hand-first.
  • Snap Spectacles — the Browser Lens: immersive-ar with hand tracking, compute-limited.

Why this fits a 2.5-day hackathon

  • The reference dialect — most WebXR snippets, Stack Overflow answers, and AI-generated code assume three.js, so help is everywhere.
  • No build step — the import-map starter runs from a single file; reach for Vite only when the project grows.
  • One scene, every headset — the same URL runs on Quest 3, PICO, Vision Pro, and Spectacles.
  • A path to splats and video — three.js loads Gaussian splats and plays stereo video in the same scene graph, so a WebXR viewer and an interactive demo share one codebase.

Questions? Reach the team via the Contact page.

// BACK TO NEWS