Depth sensing makes a virtual object hide correctly behind a real sofa. Spatial anchors make it stay put between sessions. Neither matters if you can’t first answer the question both of them assume is already solved: where, exactly, is the real world in front of this user? That’s what the WebXR Hit Test API is for — the step that turns “the user tapped the screen” into “here is a real-world point the user meant.”
What a hit test actually does
A hit test fires a ray from the device (or from a tapped screen point) outward and asks WebXR’s underlying tracking system where that ray intersects real-world geometry — a floor, a wall, a tabletop that the device has already reconstructed. This is the mechanism behind every “tap to place” AR demo, including the Immersive Web Working Group’s own reference sample, which places a sunflower wherever the user taps.
Step 1 — Request the feature
const session = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['hit-test'],
});
Unlike depth sensing, hit testing is foundational enough to most AR experiences that requiredFeatures is usually the right call — an AR session that can’t place anything on the real world isn’t a reduced experience, it’s a broken one.
Step 2 — Set up a hit test source
const viewerSpace = await session.requestReferenceSpace('viewer');
const hitTestSource = await session.requestHitTestSource({ space: viewerSpace });
viewer-space hit tests cast the ray from wherever the device is pointed — the standard choice for phone-based AR, where “pointing the device” is the aiming gesture. For controller-based headsets, request the hit test source against the controller’s own targetRaySpace instead, so the ray follows the controller rather than the headset.
Step 3 — Read results per frame
function onXRFrame(time, frame) {
const results = frame.getHitTestResults(hitTestSource);
if (results.length > 0) {
const pose = results[0].getPose(refSpace);
reticle.visible = true;
reticle.matrix.fromArray(pose.transform.matrix);
} else {
reticle.visible = false;
}
}
results[0] is the closest intersection along the ray — good enough for placement in the overwhelming majority of cases. Use it to position a reticle, the standard AR affordance that shows the user where their next tap will place content before they commit to it.
Step 4 — Place on confirmation
session.addEventListener('select', () => {
if (!reticle.visible) return;
const object = createPlacedObject();
object.matrix.copy(reticle.matrix);
object.matrixAutoUpdate = false;
scene.add(object);
});
The select event fires on a screen tap (phone AR) or a controller trigger (headset AR) — the same event type across both input modes, which is why this pattern ports cleanly between a phone and a headset without rewriting the placement logic.
Where this leads
- Combine with depth sensing for occlusion once something is placed — hit testing gets content onto a surface, depth sensing keeps other real objects correctly in front of or behind it afterward.
- Combine with spatial anchors to make a hit-tested placement survive beyond the current frame, and beyond the current session, instead of drifting as the device re-localizes.
- A-Frame users: the same capability is exposed as the
ar-hit-testcomponent, firing the same semantic events without the raw WebXR plumbing above.
Hit testing, depth sensing, and anchors are the three primitives that, together, make AR content feel sited instead of pasted on. This is the one of the three that has to run first.