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.