WebXR is the right substrate for immersive work — no app store, no install, a URL is the distribution — and it has a well-earned reputation for being tedious to start. Writing it directly means handling the session request, the reference space, the render loop, the input sources, the controller profiles, the grip-versus-target-ray distinction, and the headset’s own quirks, all before anything is on screen.
The Immersive Web SDK (IWSDK) exists to compress that. This walkthrough gets you from nothing to a running scene on a headset.
Alive Studios’ first IWSDK tutorial — the whole setup in ten minutes. Their series continues through interaction, physics and deployment.
What IWSDK is, and what it is not
It sits on top of three.js and WebXR. It is not a replacement for either — your meshes, materials and shaders are still three.js, and you can drop to the WebXR API when you need to.
What it adds is the layer everyone writes by hand and nobody enjoys writing:
- Session and reference-space management, including the immersive-vr / immersive-ar distinction
- An entity-component-system scene model rather than a bare object graph
- Input abstraction across hand tracking and controllers, so one code path serves both
- Locomotion — teleport and smooth movement, with comfort options
- Grab and interaction primitives
- Physics integration
- AR features including passthrough and scene meshing
The design decision worth understanding before you start is the ECS.
Why the entity-component model changes how you write
In plain three.js you build a scene graph and then attach behaviour to it, usually as code in your animation loop that reaches into objects by reference. That works until the number of interacting behaviours grows, at which point your render loop is a thousand lines of special cases and nothing can be reused.
In an ECS:
- An entity is an identity with no behaviour of its own
- A component is pure data attached to an entity —
Grabbable,RigidBody,Interactable - A system is logic that runs each frame over every entity with a given set of components
The practical consequence: making an object grabbable is adding a component, not writing grab code. The grab system already exists and already handles both hands and both controllers. Your job becomes deciding which entities have which components.
This is unfamiliar if you have only written scene-graph code, and it is the single biggest adjustment in moving to IWSDK. It is also why the SDK can give you interaction for free — the behaviours are written once against components, not once per object.
Step 1 — Project setup
IWSDK is a standard npm package in a bundler-based project. Vite is the usual choice and the one the SDK’s own templates assume.
npm create vite@latest my-xr-app -- --template vanilla-ts
cd my-xr-app
npm install
npm install @iwsdk/core
Do not install three separately. IWSDK depends on three aliased to super-three — a fork carrying WebXR-related patches — and installing stock three.js alongside it gives you two copies of the library and a scene where nothing matches anything. Import three.js classes from your IWSDK install, not from a second dependency.
Two things to get right immediately, because both will cost you an hour if you do not:
HTTPS is mandatory. WebXR refuses to start a session on an insecure origin. localhost is exempt, which is why the dev server works — but the moment you test from the headset over your network, you need TLS. Vite’s --host plus a self-signed certificate, or a tunnelling service, both work.
Check navigator.xr before assuming anything. On a desktop browser without a headset there is no xr object at all, and code that dereferences it throws before your scene ever loads. Feature-detect and fall back to a flat 3D view.
Step 2 — A world and a session
The SDK’s initialisation gives you the renderer, the XR session plumbing and the world in one call. The shape of it:
import { World, SessionMode } from '@iwsdk/core';
const world = await World.create(document.getElementById('app')!, {
xr: {
sessionMode: SessionMode.ImmersiveVR,
offer: 'once',
},
features: {
grabbing: true,
locomotion: true,
},
});
The package reached 1.0.1; check the current API against the SDK docs, since the surface is still moving — but the structure holds: declare the session mode, declare which features you want, get a world back.
offer: 'once' matters. A WebXR session must be started by a user gesture; the browser will not let you enter immersive mode on page load. The “offer” mechanism presents the entry affordance and waits for the tap. If your Enter VR button does nothing, this is almost always why.
Step 3 — An entity you can pick up
import { Mesh, BoxGeometry, MeshStandardMaterial } from 'three';
import { Grabbable, Interactable } from '@iwsdk/core';
const cube = new Mesh(
new BoxGeometry(0.2, 0.2, 0.2),
new MeshStandardMaterial({ color: 0x3366ff })
);
cube.position.set(0, 1.2, -0.6);
const entity = world.createTransformEntity(cube);
entity.addComponent(Grabbable);
entity.addComponent(Interactable);
Note the units and the placement. WebXR is metres, and the origin is the floor under a standing user in a local-floor reference space. y: 1.2 is roughly chest height for a seated adult; z: -0.6 is 60 cm in front. Build at real scale from the start — a scene authored in arbitrary units feels wrong in a headset in a way it never does on a monitor, and rescaling later breaks every physics and interaction assumption you made.
Two components, no grab code. That is the ECS payoff.
Step 4 — Hands and controllers through one path
This is the part that is genuinely painful in raw WebXR. A controller exposes a gripSpace and a targetRaySpace and a gamepad with a profile-dependent button layout. A tracked hand exposes 25 joint poses per hand and no buttons at all, so “pinch” is something you compute from the distance between thumb tip and index tip, with your own threshold and your own hysteresis.
IWSDK normalises both into select, squeeze, hover and move events on interactable entities. One handler, both input types:
entity.addEventListener('select', () => {
(cube.material as MeshStandardMaterial).color.setHex(0xff3366);
});
Design for both anyway, because they are not equivalent. Hand tracking has no haptics and no reliable contact signal — a pinch in mid-air gives the user nothing back, so your visual feedback has to carry the whole confirmation. A controller can buzz. If a gesture is confirmable only by vibration, hand users cannot tell whether it worked.
Step 5 — Locomotion, and the thing to be careful about
features: {
locomotion: { teleport: true, smooth: true },
}
Offer teleport, and default to it. Smooth locomotion moves the camera without corresponding vestibular input, which is the main cause of simulator sickness; teleport sidesteps the conflict entirely by removing the continuous motion. Plenty of users are fine with smooth movement and should be able to choose it — but the default should be the one that does not make a first-time visitor feel ill thirty seconds in.
Also constrain the floor. Teleport onto arbitrary geometry gets users inside walls and under the ground, which they do immediately and cannot recover from.
Step 6 — Physics and AR meshing
Enabling physics gives entities with a rigid-body component real dynamics, which is where a scene starts feeling like a place rather than a diagram — thrown objects behave, stacked objects stack.
AR meshing is the more interesting half. In ImmersiveAR mode with passthrough, the headset supplies a mesh of the actual room, and your content can collide with your actual floor and your actual table. A ball that bounces off your real coffee table is a categorically different experience from one that bounces off an invisible plane, and it costs almost nothing once the meshing data is wired in.
The caveat: the room mesh is approximate and arrives late. It is a coarse reconstruction, it may not include thin or reflective objects, and it is not available at frame zero. Write the fallback — content that works on a flat assumed floor — and let the mesh refine it when it lands.
Step 7 — Getting it onto a headset
Two routes, both useful:
Local network. Run the dev server bound to your LAN with HTTPS, then open the address in the headset browser. Fastest iteration loop in XR development: save, look up, reload.
Deployed. Any static host with TLS serves a built IWSDK project. Because it is the open web, the result is a link — which you can send to someone who then experiences your piece without installing anything. That property remains WebXR’s strongest argument.
Where to go next
- Interaction depth. Constrained grabs — hinges, sliders, two-handed holds — are where an XR interface starts feeling designed rather than generic.
- Performance. The budget is brutal: two eyes at 72–120 Hz. Measure early, keep draw calls low, and treat transparency and post-processing as luxuries.
- Comfort as a feature, not a setting. Teleport defaults, a visible horizon, no camera motion you did not initiate, and no text closer than about half a metre.
- Hands-only as a target. Build something that works with no controllers at all. It is the direction the hardware is going, and it forces honesty about whether your interactions are discoverable.