Skip to main content
graphwiz.ai
← Back to Posts

WebXR Anchors: Creating Persistent AR Experiences

XRWebXR
webxranchorsarspatialpersistence

WebXR Anchors: Creating Persistent AR Experiences

Anchors solve a fundamental AR problem: keeping virtual content attached to real-world locations. Without anchors, AR objects drift as you move.

The Anchor Problem

Traditional AR places objects relative to the camera. As you walk around:

  • Objects don't stay "pinned" to real locations
  • Returning to a spot doesn't show previous content
  • Multi-user experiences are nearly impossible

Anchors solve all three.

Anchor API

Check Support

const session = await navigator.xr.requestSession('immersive-ar', {
  requiredFeatures: ['anchors']
});

const anchorsSupported = session.enabledFeatures?.includes('anchors');

Create Anchor

async function createAnchor(position, orientation) {
  const anchor = await session.addAnchor(
    new XRRigidTransform(position, orientation)
  );
  return anchor;
}

Anchor from Hit Test

async function placeAnchorAtTap() {
  const hitTestSource = await session.requestHitTestSource({ space: viewerSpace });

  session.requestAnimationFrame((time, frame) => {
    const results = frame.getHitTestResults(hitTestSource);

    if (results.length > 0) {
      const pose = results[0].getPose(referenceSpace);
      const anchor = await frame.createAnchor(pose, referenceSpace);
      placeObjectAtAnchor(anchor);
    }
  });
}

Working with Anchors

Store Anchor Pose

const anchors = new Map();

function trackAnchor(anchor, objectId) {
  anchors.set(anchor, {
    id: objectId,
    createdAt: Date.now()
  });
}

function updateAnchors(frame, referenceSpace) {
  for (const [anchor, data] of anchors) {
    const pose = frame.getPose(anchor.anchorSpace, referenceSpace);
    if (pose) {
      updateObjectPosition(data.id, pose.transform);
    }
  }
}

Remove Anchor

function removeAnchor(anchor) {
  anchor.delete();
  anchors.delete(anchor);
}

Persistence Across Sessions

Save Anchor Data

async function saveAnchors() {
  const anchorData = [];

  for (const [anchor, data] of anchors) {
    // Get current pose
    const pose = frame.getPose(anchor.anchorSpace, referenceSpace);

    anchorData.push({
      id: data.id,
      position: pose.transform.position,
      orientation: pose.transform.orientation,
      timestamp: Date.now()
    });
  }

  // Save to localStorage or cloud
  localStorage.setItem('arAnchors', JSON.stringify(anchorData));
}

Restore Anchors

async function restoreAnchors() {
  const savedData = JSON.parse(localStorage.getItem('arAnchors') || '[]');

  for (const data of savedData) {
    const transform = new XRRigidTransform(
      data.position,
      data.orientation
    );

    const anchor = await frame.createAnchor(transform, referenceSpace);

    placeObjectAtAnchor(anchor, data.id);
  }
}

World Tracking

Persistent Anchor IDs

Some devices support persistent anchor IDs:

async function checkPersistence() {
  if ('requestPersistentAnchor' in session) {
    // Persistent anchors supported
    const anchor = await session.requestPersistentAnchor('room-center');
  }
}

Cloud Anchor Services

For cross-device persistence:

// Using a cloud anchor service
async function shareAnchor(anchor) {
  const pose = frame.getPose(anchor.anchorSpace, referenceSpace);

  const response = await fetch('/api/anchors', {
    method: 'POST',
    body: JSON.stringify({
      position: pose.transform.position,
      orientation: pose.transform.orientation,
      environmentData: await captureEnvironmentMap()
    })
  });

  const { anchorId } = await response.json();
  return anchorId;
}

Complete Example

class ARAnchorManager {
  constructor(session) {
    this.session = session;
    this.anchors = new Map();
    this.objects = new Map();
  }

  async placeObject(position, object) {
    const transform = new XRRigidTransform(position);
    const anchor = await this.session.addAnchor(transform);

    this.anchors.set(anchor, object);
    this.objects.set(object.id, anchor);

    return anchor;
  }

  update(frame, referenceSpace) {
    for (const [anchor, object] of this.anchors) {
      const pose = frame.getPose(anchor.anchorSpace, referenceSpace);
      if (pose) {
        object.position.copy(pose.transform.position);
        object.quaternion.copy(pose.transform.orientation);
      }
    }
  }

  removeObject(objectId) {
    const anchor = this.objects.get(objectId);
    if (anchor) {
      anchor.delete();
      this.anchors.delete(anchor);
      this.objects.delete(objectId);
    }
  }

  save() {
    const data = [];
    for (const [anchor, object] of this.anchors) {
      data.push({
        objectId: object.id,
        // Position in world coordinates
      });
    }
    localStorage.setItem('arAnchors', JSON.stringify(data));
  }
}

Browser Support

Feature Chrome Safari Edge
Basic Anchors 85+ 16+ 85+
Persistent Anchors Limited 16+ Limited
Hit Test 79+ 16+ 79+

Best Practices

  1. Create anchors sparingly - They consume resources
  2. Clean up deleted anchors - Call anchor.delete()
  3. Handle tracking loss - Anchors may become invalid
  4. Test on real devices - Desktop AR doesn't exist

Conclusion

Anchors transform AR from a novelty into a practical technology. Content that persists in real locations enables navigation, gaming, and industrial applications.