Back openDesk Edu for a sovereign, open-source education — every vote counts.
Vote nowSave products you love by clicking the heart icon.
Design principles and implementation patterns for building intuitive, accessible spatial user interfaces in WebXR — from layout and typography to gestures and feedback.
The WebXR ecosystem in 2026 spans a wide range of devices — from Meta Quest 3 and Apple Vision Pro to handheld mobile AR and desktop VR headsets. Each platform has unique capabilities, input methods, and performance profiles. Building cross-platform WebXR requires a progressive enhancement approach, not a one-size-fits-all solution.
| Device | Input | Tracking | Performance | WebXR Support |
|---|---|---|---|---|
| Meta Quest 3/3S | Touch controllers, hands | Inside-out 6DOF | Mid (XR2 Gen 2) | Full WebXR |
| Apple Vision Pro | Eyes, hands, voice | Inside-out, eye tracking | High (M2 + R1) | WebXR via Safari 18 |
| Mobile AR (iOS/Android) | Touch screen | SLAM | Varies (device GPU) | WebXR AR only |
| PlayStation VR2 | PSVR2 controllers | Inside-out | High | WebXR via browser |
| Desktop VR (Index, etc.) | Motion controllers | Lighthouse/inside-out | High | Full WebXR |
Don't assume capabilities — detect them at runtime:
async function detectXRCapabilities() {
if (!navigator.xr) return { supported: false, reason: "No WebXR" };
const supportedModes = {
inline: await navigator.xr.isSessionSupported("inline"),
immersiveVR: await navigator.xr.isSessionSupported("immersive-vr"),
immersiveAR: await navigator.xr.isSessionSupported("immersive-ar"),
};
// Detect specific features
const features = {
handTracking: supportedModes.immersiveVR && "hand-tracking" in XRSession.prototype,
hitTest: "XRHitTestSource" in window,
anchors: "XRAnchor" in window,
depthSensing: "XRDepthInformation" in window,
layers: "XRCompositionLayer" in window,
eyeTracking: "XREye" in window,
};
return { supportedModes, features };
}
Layer capabilities from basic to advanced:
function XRApp() {
const [capabilities, setCapabilities] = useState<XRCapabilities | null>(null);
useEffect(() => {
detectXRCapabilities().then(setCapabilities);
}, []);
if (!capabilities) return <LoadingScreen />;
if (!capabilities.supportedModes.immersiveVR && !capabilities.supportedModes.immersiveAR) {
return <Fallback3DView />;
}
return (
<XRRenderer capabilities={capabilities}>
<SpatialUI inputMethod={detectInputMethod(capabilities)} />
<SceneGraph quality={selectQuality(capabilities)} />
</XRRenderer>
);
}
Abstract input so UI logic doesn't depend on device:
type XRInputEvent = {
position: THREE.Vector3;
direction?: THREE.Quaternion;
type: "select" | "squeeze" | "pinch" | "gaze" | "touch";
source: "controller" | "hand" | "gaze" | "screen";
};
function createInputManager(capabilities: XRCapabilities) {
const input$ = new Subject<XRInputEvent>();
if (capabilities.features.handTracking) {
setupHandTracking(input$);
}
if (capabilities.supportedModes.immersiveVR) {
setupControllers(input$);
}
// Always add gaze fallback
setupGazeInput(input$);
return input$;
}
Quest has excellent WebXR support but limited GPU. Key strategies:
// Quest-specific optimization: fixed foveated rendering
const xrSession = await navigator.xr.requestSession("immersive-vr", {
optionalFeatures: ["foveation"],
});
if (xrSession.updateRenderState) {
xrSession.updateRenderState({
foveationLevel: 0.5, // 0=off, 1=maximum
});
}
Use passthrough AR on Quest 3 for mixed reality experiences:
const session = await navigator.xr.requestSession("immersive-ar", {
optionalFeatures: ["passthrough"],
});
Vision Pro Safari 18 supports WebXR with unique constraints:
// Vision Pro: use eye tracking for precision targeting
const eyeGazeSource = await session.requestReferenceSpace("local");
const eyeTracker = await session.requestAnimationFrame((time, frame) => {
const pose = frame.getPose(eyeGazeSpace, referenceSpace);
if (pose) highlightTarget(pose.transform.position);
});
// Vision Pro: handle direct touch input
element.addEventListener("touchstart", (e) => {
// Vision Pro reports touches as pointer events from eye + pinch
});
Key Vision Pro considerations:
Mobile AR is the widest reach but most constrained:
if (sessionMode === "immersive-ar" && isMobileDevice()) {
// Mobile AR: flat surface placement is the primary interaction
session.requestHitTestSource({ space: viewerSpace });
// Limit draw calls to ~30
// Use half-resolution rendering
gl.setSize(window.innerWidth / 2, window.innerHeight / 2);
}
Scale rendering quality based on device capability:
function selectQuality(device: XRCapabilities): QualitySettings {
const gpuTier = detectGPUTier();
if (gpuTier === "high") {
return { pixelRatio: 1, shadows: true, postProcessing: true, msaa: 4 };
}
if (gpuTier === "medium") {
return { pixelRatio: 0.75, shadows: false, postProcessing: false, msaa: 2 };
}
// Low-end (mobile)
return { pixelRatio: 0.5, shadows: false, postProcessing: false, msaa: 1 };
}
Test across devices in CI:
jobs:
webxr-test:
strategy:
matrix:
device: [quest-3, vision-pro, mobile-ios, mobile-android]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- uses: WebXR-Test/action@v2
with:
device: ${{ matrix.device }}
url: https://staging.example.com/xr-app
tests: tests/webxr/
Always provide a non-XR experience:
function Fallback3DView() {
return (
<div className="relative h-screen w-full">
<canvas ref={canvasRef} /> {/* Three.js fallback */}
<div className="absolute bottom-8 left-1/2 -translate-x-1/2">
<p className="text-sm text-gray-500">
WebXR not supported on this device. View the 3D scene in your browser instead.
</p>
</div>
</div>
);
}
Before shipping cross-platform XR:
Cross-platform WebXR in 2026 means supporting a wider range of devices than ever. Test on real hardware, not just emulators.