Spatial UI & Interaction Design for WebXR
Spatial UI & Interaction Design for WebXR
Spatial UI moves beyond flat screens into 3D environments where users interact with content using their hands, gaze, and controllers. Designing for this medium requires rethinking layout, typography, feedback, and accessibility from the ground up.
This guide covers proven patterns for building spatial interfaces in WebXR, with practical code examples and design principles drawn from production applications.
Why Spatial UI Is Different
| Screen UI | Spatial UI |
|---|---|
| Fixed viewport | User moves around content |
| Mouse/touch input | Hands, gaze, controllers, voice |
| CSS pixels | Real-world meters |
| 2D layout | 3D placement with depth |
| No physical fatigue | Comfort and ergonomics matter |
The core rule: spatial UI should feel natural in the real world, not like a flat screen floating in VR.
Layout Fundamentals
Units and Scale
Use real-world units for spatial layout:
// 1 meter ≈ 1 unit in WebXR
const UI_DISTANCE = 1.5 // arm's reach
const BUTTON_SIZE = 0.08 // 8cm button
const TEXT_HEIGHT = 0.03 // 3cm text for readability
A button smaller than 5cm at arm's length is hard to tap. Text below 2cm becomes unreadable. Test your layouts at real-world distances.
The Viewport Funnel
Structure content in zones based on user attention:
- Focal zone (0–30° center) — Primary content, confirmations
- Peripheral zone (30–60°) — Secondary info, status
- Background zone (60°+) — Ambient environment, not interactive
// Spatial zones helper
const ZONES = {
focal: { x: 0, y: 0, z: -1.5 },
peripheral: { x: 1.2, y: 0.3, z: -1.5 },
background: { x: 0, y: 0, z: -5 },
}
Comfortable Reading
Use a curved billboard for text panels so every word is equidistant from the user:
const textPanel = new THREE.Mesh(
new THREE.CylinderGeometry(0.8, 0.8, 0.5, 32, 1, true),
textMaterial
)
textPanel.rotation.x = -Math.PI / 2
For dashboards and data-heavy UIs, consider a scrollable panel that the user reaches out to interact with, similar to a physical tablet.
Interaction Patterns
Direct Manipulation
Users should grab, tap, and drag objects as they would in the real world:
// Three.js drag interaction
controller.addEventListener('select', () => {
const hit = raycast(intersectables)
if (hit) {
hit.object.userData.onSelect?.()
}
})
controller.addEventListener('squeeze', () => {
const grabbed = raycast(grabbables)
if (grabbed) {
attachToController(grabbed.object, controller)
}
})