WebXR Anchors Pin AR Objects to Real Surfaces Without Drift

Augmented Reality
Date:October 12, 2026
Topic:
WebXR Anchors Pin AR Objects to Real Surfaces Without Drift
⏱ 3 min read

Your AR prototype works perfectly in the living room. Ship it to a client site and the virtual monitor drifts three feet left by lunch. The hologram anchored to the conference table? Floating near the ceiling by afternoon. This isn't a tracking bug. It's the fundamental problem of world-locked content without persistent anchors.

Why Drift Happens

WebXR's default reference spaces (local, local-floor, bounded-floor) are relative to the session start pose. The headset builds a map of the room as you move, constantly refining its understanding of where the floor and walls actually are. Every refinement shifts the coordinate system. Your content, parented to that shifting frame, slides along with it.

Plane detection helps you place objects initially, but planes update. A detected floor plane might rise 2cm when the user walks to the far wall. The virtual keyboard on that plane rises with it.

Spatial Anchors Lock Content to Reality

A spatial anchor represents a fixed pose in the real world that the runtime maintains across session resumptions. When you create an anchor at a specific hit-test result, the runtime ties that pose to its internal map of the space. As the map refines, the anchor updates to stay glued to the same physical point.

This works today in stable WebXR on Quest, Vision Pro, Android (ARCore), and iOS (ARKit). No experimental flags required.

javascript
// Create anchor at hit-test position
const hitTestSource = await xrSession.requestHitTestSource({ space: viewerSpace });
const hitTestResults = frame.getHitTestResults(hitTestSource);
if (hitTestResults.length > 0) {
  const hitPose = hitTestResults[0].getPose(xrReferenceSpace);
  const anchor = await xrSession.createAnchor(hitPose);
  
  // Store anchor UUID for persistence
  localStorage.setItem('monitorAnchor', anchor.uuid);
  
  // Attach your Three.js/Babylon object to anchor's anchorSpace
  object.matrixAutoUpdate = false;
  function updatePose(frame) {
    const anchorPose = frame.getPose(anchor.anchorSpace, xrReferenceSpace);
    if (anchorPose) {
      object.matrix.fromArray(anchorPose.transform.matrix);
    }
  }
}
💡
TipAlways check anchor.trackingState. If it becomes 'unstable', the runtime lost confidence. Hide content or show a 'relocate' prompt instead of rendering drifting garbage.

Persist Anchors Across Sessions

The anchor UUID is your key to persistence. Store it with your scene data. On next session, request the anchor by UUID before rendering.

javascript
// Restore anchor on session start
const savedUuid = localStorage.getItem('monitorAnchor');
if (savedUuid) {
  try {
    const anchor = await xrSession.getAnchor(savedUuid);
    if (anchor) {
      // Anchor found - attach content immediately
      attachContentToAnchor(anchor);
    } else {
      // Anchor gone (map cleared, different room) - fallback to placement UI
      showPlacementUI();
    }
  } catch (e) {
    console.warn('Anchor restore failed:', e);
    showPlacementUI();
  }
}

Plane Detection + Anchors = Robust Placement

Don't choose one. Use planes for discovery and anchors for commitment.

  1. Run plane detection to visualize surfaces and guide user placement
  2. On user confirm, create anchor at the hit-test pose on that plane
  3. Stop plane detection (saves CPU) and rely on anchor for pose
  4. If anchor becomes unstable, restart plane detection for re-anchoring

This pattern handles the full lifecycle: discover → place → persist → recover.

ScenarioWithout AnchorsWith Anchors
Session resumeContent floats at originContent snaps to physical spot
Map refinement (walking)Drift accumulatesPose corrected per frame
Different roomContent stuck in wrong placeAnchor missing → graceful fallback
Multi-device (same space)Manual alignment neededShared anchor UUIDs align instantly

Common Pitfalls

Creating anchors every frame. Anchors are heavy. Create once per placement action. Reuse the anchorSpace for rendering.

Ignoring trackingState. An anchor can go from 'tracking' to 'unstable' if the runtime loses the map region. Always read it in your render loop.

Assuming anchors survive map clears. User resets guardian? Anchors gone. User switches headset accounts? Anchors gone. Design your fallback UX first.

Over-anchoring. 50 anchors in a room taxes the runtime. Cluster nearby objects under one parent anchor when possible.

"

The teams shipping reliable AR in 2026 treat anchors as infrastructure, not features. They build the persistence layer once and every experience inherits stability.

— WebXR Working Group participant

Your Next Steps

1. Add anchor creation to your placement flow this week. 2. Implement UUID storage and restoration. 3. Build the 'anchor lost' recovery UI before you need it. 4. Test across a full day: place at 9am, walk away, resume at 2pm, resume tomorrow.

The difference between a demo and a product is whether the virtual monitor is still on the desk when the client returns from lunch.


âœĻ
â„šī¸
NoteSpec reference: WebXR Anchors Module (w3c.github.io/webxr-anchors). Polyfill for older runtimes: github.com/immersive-web/webxr-polyfill/tree/main/packages/anchors
Share𝕏 Twitterin LinkedInin Whatsapp