Tutorial · Intermediate · ~40 min
WebXR · AR · Hit Testing · Plane Detection

AR in Browsers: Plane Detection & Hit Testing

On Android Chrome, WebXR exposes the camera feed, SLAM tracking, and plane detection — no app install needed. Hit testing casts a ray from the screen centre and reports intersection with detected surfaces, letting you place virtual objects on real floors and tables.

1Request an immersive-ar session

const supported = await navigator.xr?.isSessionSupported('immersive-ar'); if (!supported) { document.getElementById('ar-btn').textContent = 'AR not available'; return; } let xrSession, hitTestSource, refSpace; document.getElementById('ar-btn').addEventListener('click', async () => { xrSession = await navigator.xr.requestSession('immersive-ar', { requiredFeatures: ['hit-test'], optionalFeatures: ['plane-detection', 'dom-overlay'], domOverlay: { root: document.getElementById('overlay') }, }); await renderer.xr.setSession(xrSession); // Set up reference space (viewer = headset, local = world) refSpace = await xrSession.requestReferenceSpace('local'); // Request a hit-test source relative to the viewer's gaze const viewerRef = await xrSession.requestReferenceSpace('viewer'); hitTestSource = await xrSession.requestHitTestSource({ space: viewerRef }); xrSession.addEventListener('end', cleanup); });
The dom-overlay feature lets you overlay HTML (buttons, scores, labels) on top of the AR view — essential for a polished AR UI.

2Transparent background for AR

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js'; const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, // transparent canvas so camera shows through }); renderer.setPixelRatio(devicePixelRatio); renderer.xr.enabled = true; // Scene background must be null (transparent) — not black const scene = new THREE.Scene(); scene.background = null; // Light that works in AR (HemisphereLight approximates real-world env) scene.add(new THREE.HemisphereLight(0xffffff, 0xbbbbff, 1)); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); dirLight.position.set(0.5, 1, 0.866); scene.add(dirLight); const camera = new THREE.PerspectiveCamera();

3Hit testing from screen centre

The hit-test source fires a ray from the viewer's gaze direction. In the render loop, check intersection results:

// Reticle — shows where the object will land const reticleGeo = new THREE.RingGeometry(0.05, 0.07, 32).rotateX(-Math.PI / 2); const reticle = new THREE.Mesh(reticleGeo, new THREE.MeshBasicMaterial({ color: 0x22c55e })); reticle.visible = false; reticle.matrixAutoUpdate = false; scene.add(reticle); renderer.setAnimationLoop((time, frame) => { if (frame && hitTestSource) { const results = frame.getHitTestResults(hitTestSource); if (results.length > 0) { const hit = results[0]; const pose = hit.getPose(refSpace); reticle.visible = true; reticle.matrix.fromArray(pose.transform.matrix); } else { reticle.visible = false; } } renderer.render(scene, camera); });

4Place an object on tap

xrSession.addEventListener('select', () => { if (!reticle.visible) return; // Clone the reticle matrix to place an object at the hit point const pos = new THREE.Vector3(); const quat = new THREE.Quaternion(); const scl = new THREE.Vector3(); reticle.matrix.decompose(pos, quat, scl); const obj = new THREE.Mesh( new THREE.BoxGeometry(0.1, 0.1, 0.1), new THREE.MeshStandardMaterial({ color: 0xff4444 }) ); obj.position.copy(pos); obj.quaternion.copy(quat); scene.add(obj); // Optional: play a haptic click const gp = xrSession.inputSources[0]?.gamepad; gp?.hapticActuators?.[0]?.pulse(0.3, 30); });

5Plane detection and visualisation

// Detect detected planes and draw a semi-transparent overlay const planeMap = new Map(); // XRPlane → THREE.Mesh renderer.setAnimationLoop((time, frame) => { if (!frame) return; // detectedPlanes is a Set of XRPlane objects (requires 'plane-detection') const planes = frame.detectedPlanes ?? new Set(); for (const plane of planes) { const pose = frame.getPose(plane.planeSpace, refSpace); if (!pose) continue; if (!planeMap.has(plane)) { // Build a polygon mesh from the plane's boundary points const pts = plane.polygon; // Array of {x,y,z} in plane space const shape = new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.z))); const geo = new THREE.ShapeGeometry(shape); const mat = new THREE.MeshBasicMaterial({ color: 0x22c55e, side: THREE.DoubleSide, transparent: true, opacity: 0.2, }); const mesh = new THREE.Mesh(geo, mat); mesh.rotation.x = -Math.PI / 2; scene.add(mesh); planeMap.set(plane, mesh); } const mesh = planeMap.get(plane); mesh.matrix.fromArray(pose.transform.matrix); mesh.matrixAutoUpdate = false; } renderer.render(scene, camera); });
Plane detection is supported on Android Chrome with ARCore. On iOS, Safari supports WebXR AR via the worldSensing feature flag available in Safari 17+.