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+.

Frequently Asked Questions

What will I learn in this tutorial?

Use WebXR hit-testing and plane detection to place 3D objects on real surfaces in mobile AR — no app install required.

What topics are covered in this tutorial?

This tutorial covers: Request an immersive-ar session, Transparent background and AR camera, Hit testing from screen centre, Place an object on tap, Plane detection and visualisation.

What tools and technologies does this tutorial use?

This tutorial uses WebXR, AR, Hit Testing, Plane Detection.

How long does this tutorial take?

This tutorial takes approximately 40 minutes to complete.

What prerequisites do I need before starting?

This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.