Blog
WebXRShared SpacesMultiplayerMeta QuestColocationGame Development

Shared Spaces in WebXR: Two Headsets, One Coordinate System

ByFlorian Isikci·Founder, dmnshd··10 min read
Two players in the same room wearing Meta Quest headsets, both leaning over the same miniature racing track on a real coffee table in passthrough

Shared spaces is an optional WebXR feature that lets two (or more) headsets in the same room agree on a single coordinate system, so a virtual object lands in the same physical spot for both players. Here is what the browser gives you, what it deliberately leaves to you and how we wired it into Shed Racer and our latest release, Rebound

The part that used to be hard

Picture two people in the same room, each wearing a Quest. On the real coffee table between them sits a tiny racing track, the RC-car-sized circuit that Shed Racer drops onto any flat surface in passthrough. Both of them see it. Not two separate copies, one floating somewhere in each headset, but the same track in the same place, so when one player leans over and the other reaches in to point at that corner, they are pointing at the same patch.

That last part is the one that used to be hard. Two headsets in the same room do not naturally agree on where anything is. Each one builds its own map of the space and picks its own origin when the session starts, so your "two meters forward" and my "two meters forward" land in completely different places. Put a virtual object at coordinate (0, 0, 0) on both and it shows up in two different corners of the room.

Getting them to agree is called colocation and it was only possible on the web through hacky methods. The usual workarounds were all some version of asking the players to do the agreeing for you. Stand on this spot and press a button. Touch your controllers together. They sort of work, they are fiddly to set up, they drift and they are exactly the kind of friction WebXR is supposed to delete. So most browser games just never did local multiplayer in the same room (most still don't as it is still behind a flag).

That changed quietly when the Quest browser shipped an experimental feature called shared spaces and it is the reason two people can now share a coffee table in Shed Racer. The feature exposes the native Shared Spaces to the browser.

What the browser gives you

Shared spaces is a proposal from Rik Cabanier, an engineer on Meta's browser team and the whole idea fits in a sentence: the browser hands you a reference space that is already lined up with every other connected headset in the room.

The API is deliberately tiny. You add "shared" to your session's optional features when you call requestSession and once the session is running you ask for a "shared" reference space the same way you would ask for "local-floor" or "viewer". What comes back is an ordinary reference space with one extra property attached to it: a UUID. Every headset in the same physical room that asks for a shared space gets the same coordinate system and the same UUID. That is the entire trick. Once two headsets share an origin, a position in my world is the same position in yours and we can pass coordinates back and forth without translating anything.

A few properties are worth knowing before you build on it, because they shape what you can and cannot do:

  • It is bound to a room. Headsets that are far apart will not join the same space, and a headset that walks closer can start participating. The origin is set by whoever created the space first and everyone who joins inherits it.
  • It is scoped to your site. The shared space for yoursite.com/a is not the shared space for yoursite.com/b. Each page gets its own space and its own UUID, so two unrelated experiences in the same room never collide.
  • It takes a moment to settle. When the session starts, the browser may hand you a placeholder space for a second or two while it works out the real one. Once it locks in, a reset event fires with the correct coordinate system and UUID (unless you were the first headset in, in which case yours was right from the start). In the current build the UUID also tends to get populated in place rather than firing that event, so in practice you poll it until it shows up. The reference sample does exactly that, with the onReset line sitting commented out.
  • It is ephemeral. Leave the immersive session and the shared space is gone; you re-establish it on the way back in. When the last headset leaves, the space disappears entirely.

On the spec side there is not much to it: shared becomes a new reference space type and there is a new XRSharedReferenceSpace object that is just a reference space with a UUID on it. Nothing else in your rendering code has to change, because a shared reference space behaves like any other one. You set it as your reference space and your framework of choice keeps drawing the way it always did. The only difference is that "the origin" now means the same physical point for everybody.

Diagram: two Quest headsets in the same room, each with its own local origin on the left, converging on a single shared origin and a matching UUID on the right, with position coordinates that now line up between them

The part the browser does not give you

Here is a thing that confused me at first: shared spaces does not do multiplayer for you. It does not connect anybody to anybody. It just hands every headset in the room a matching coordinate system and a UUID and then it steps back and lets you figure out the rest.

That sounds like a gap, but it is the right call for an upcoming standard. Networking is opinionated. Some games want peer-to-peer, some want an authoritative server, some are fine with a relay. The browser staying out of that means you bring whatever you already have. What the UUID gives you is the missing piece that was always annoying to build yourself: a reliable way to know which players are in the same room. Two headsets with the same shared-space UUID are, by definition, looking at the same physical space.

So the pattern everybody converges on looks the same: send your shared-space UUID plus your network ID up to your matchmaking layer of choice. Get back the list of other players reporting the same UUID. Connect to them however you like. Then start exchanging poses in the shared coordinate space and because the origin is common, you can send a position straight across the wire and trust that it means the same place on the other end. Cabanier's demo game uses PeerJS for exactly this and ships headset and controller positions every few seconds; there is a short clip of him playing it with his daughter that sells the idea better than any spec text.

How Shed Racer uses it

Shed Racer already had networked multiplayer over PeerJS, which I wrote about when the game shipped. Adding shared spaces was less "build multiplayer" and more "let the room decide where the track goes," and it came out to three parts: ask for the feature, detect whether you actually got it and put it behind a switch.

Asking is cheap, because it is optional. When the game starts an immersive session it lists shared among the optional features, right next to local-floor and hand-tracking. Optional is the operative word. On a headset or browser that does not support shared spaces, the feature is silently dropped and the session starts normally, with everything else intact. There is no separate code path for "supports colocation" versus "doesn't," and nobody gets locked out of the game because their browser is a version behind. That matters a lot for a feature this experimental.

Detecting it takes two checks. After the session starts, the game looks at session.enabledFeatures to see whether shared survived the request. That tells you the browser was willing. Whether you really have a shared space comes down to the next step: calling requestReferenceSpace("shared") and seeing whether it resolves. If it does, you have a shared space and you start watching its UUID; if it rejects, the browser almost certainly has the experimental flag turned off and the game quietly falls back to ordinary multiplayer. The UUID is the real signal, because matching two players is just comparing strings: same UUID means same room. An empty UUID means the space has not settled yet, so the game keeps polling until it fills in, for the reasons mentioned above.

Then it goes behind a switch. This is the bit I want to emphasize, I don't think colocation is something you should silently force on people (unless your experience advertises it of course). In the multiplayer lobby there is a toggle labeled Shared Space: "Co-locate the track in your room." It only appears when it could actually do something, which means at least two players who are in VR or AR, in the same room (matching UUID) and on a browser that granted the feature. When you are playing solo, or the other player is on a phone, or somebody's flag is off, the toggle either hides itself or greys out. Opting in is per-player and gets broadcast to the others, so colocation only kicks in once enough people in the same room have opted in.

Once a race starts with shared space on, one player is picked as the leader (the host if they qualify, otherwise just a stable pick). The leader reads where their head is in the shared space, drops the track origin a little below eye level and centered on the layout and broadcasts that single position to everyone else. Because every headset is using the same shared reference space, that one coordinate is all it takes; everyone places the track at the same physical spot and it rebroadcasts for a few seconds so anyone still loading snaps into alignment. From then on each player streams their head pose into the shared space about twenty times a second and the others render it as a floating helmet. That is the whole point of it: you glance to your right and there is your friend's helmet, hovering over the same table, looking at the same corner you are.

Two colocated players in Shed Racer: the track sits on the same real table for both, and each player's head shows up as a floating helmet on the other side.

One small design decision falls out of this. When you are colocated, Shed Racer locks you into the shared tabletop view and disables the first-person toggle. A shared physical table only makes sense if everyone is looking at the same shared table; letting one player drop into a cockpit while the other stays in tabletop would break the illusion the whole feature exists to create.

First-person passthrough view inside a Quest in Shed Racer: a miniature racing track sits on a real table, with a floating helmet representing another colocated player hovering over the far side of the track

Doing it again with Rebound

We did this a second time with Rebound, a port of an old Google ping-pong experiment to native WebXR, and it turned out easier than I expected for a reason that has nothing to do with shared spaces. A ping-pong table has two ends. The players stand at opposite ones, facing each other across the net, which sounds like the harder version of the problem, not the easier one. But Rebound was already built on a mirror. Each player's game treats itself as the near side of the table and the opponent as a reflection across the center, so paddle and ball go over the wire relative to the table, never to the room. Dropping a shared space in came down to the host placing the table once and the joiner taking that same placement flipped 180 degrees about the center, which is the exact reflection the netcode was already using. The game never had to learn that one of the players was now really standing on the far side of a real table. The trick it used to fake a second viewpoint just quietly became true.

Colocated Rebound in passthrough: both players stand at opposite ends of the same physical table, the net lined up between them, hitting the ball back and forth across the room.

Shared spaces works in a plain VR session as well as an AR one; we run colocated Rebound in passthrough so each player can see the real table and the person across it. The rough edge was the settle-then-reset timing, and it bit harder here than it does on a flat race track, because a ping-pong table is something you stand a fixed distance from and swing at. Place it the moment the UUIDs match and a reset a second later rotates one player's frame, and the two tables end up aimed a few degrees off from each other, which is enough that you are no longer hitting the ball toward the person you are playing. So we wait for the space to stop moving before placing anything, then let the host position the table and have both players hit a ready button before the first serve. Colocation isn't aligned the instant you join. You give it a second and a confirmation instead of trusting the first frame.

If I were starting over, the one thing I would change is the order of operations, and it is the part I would warn anyone else off. Rebound connects the two players over the network first, from the flat lobby, and only drops into XR once the match is already set up. So the whole shared-space handshake happens late: each headset goes immersive, waits the second or two it takes for a real UUID to populate, and only then has something worth sending across to the other side. By that point both players think they have already joined, so the colocation negotiation lands in the middle of "why hasn't it started yet" rather than before it. Shed Racer sidesteps this by getting you into an immersive session before it tries to colocate anything, so the UUID is already settling by the time it is needed. The fix is to treat that UUID as something you warm up early: get into XR, let the id settle, and make sure both sides have actually traded it before anyone believes they are in the same match. Establishing and communicating it after the session is supposed to be live turns a quiet two-second wait into friction the player feels.

Where it works and where it doesn't

This is the part where I temper the excitement, the same way I did with hand tracking: the support picture is narrow.

Shared spaces is Quest only and it is behind a flag. It arrived as an experiment in version 39 of the Quest browser and to enable it on you have to go to chrome://flags, search for "WebXR experiments," enable it and restart the browser. Until you do that, requestReferenceSpace("shared") just gets rejected. There is no equivalent OpenXR standard for shared spaces as of the writing of this article.

Behind the scenes, the feature is built on Meta's Shared Spatial Anchors, the same system their native colocation games use, where headsets in a room localize against a common anchor that lives in Meta's cloud. There is no vendor-neutral OpenXR extension for that yet, so there is nothing for the browser to expose to other devices. A Quest can find another Quest in the same room, but it has no standard way to find an Apple or a Samsung headset across the carpet. Until the platforms agree on a shared anchoring primitive, shared spaces on the web (or native) is going to stay a Quest-to-Quest feature.

I am genuinely fine with that for now, as it is still not finalized. The toggle simply does not show up for players who cannot use it, the game plays perfectly well without it and the people who can use it (two friends in the same room, both on Quests) get something special added on top of their experience. It degrades to nothing, which is the right behavior for anything you build on an experimental flag.

What building on a flag actually feels like

A couple of things from wiring this in are worth passing along.

The first is that you cannot meaningfully test colocation alone. Most of WebXR you can fake at your desk: Meta's immersive web emulator gets you a long way when testing for controllers, hands and many other WebXR features. Shared spaces is not like that. The entire feature is about two physical headsets agreeing on a physical room, so verifying it means two real headsets, two real people and the flag flipped on both. I spent more time than I would like to admit switching between two Quests on the same table, sweating into both of them in turn, watching whether a helmet showed up where a head actually was.

The second is that experimental means experimental. The UUID behaving differently from the proposal text, the placeholder-then-reset dance, the polling workaround: these are the rough edges of a feature that has not been battle-tested by a thousand shipped games yet, because it lives behind a flag in one browser. None of it is hard, but you feel the lack of a paved road. You are reading a one-page proposal and a reference demo and a bit of trial and error, not Stack Overflow answers from people who hit your exact bug two years ago.

And the open question I can't answer yet is whether colocated racing is even worth it. I think the moment of two people leaning over the same tiny track is special. I am not yet sure how often two people with Quests are in the same room wanting to race RC cars on a coffee table, which is a different question from whether it is technically cool. That is the sort of thing you only learn from players, not from a spec.

Where this goes

I can see where this is heading, even if I can't tell you when. The native side keeps investing in colocation, the browser team keeps shipping experiments like this one ahead of the standards process and the obvious missing piece is a vendor-neutral anchoring extension that would let a shared space span a Quest and a Vision Pro and whatever comes next.

The whole pull of the immersive web is that you tap a link and you are in, no install, no account, nothing to update. Colocation is that same promise pointed at the room instead of the headset: your friend is already next to you, so the experience should already know that. For a future of lighter passthrough glasses that you wear around other people, the version of multiplayer that needs zero setup because the room is the lobby is the one that feels native to the hardware.

Until that day comes, it is a flag, a Quest and a coffee table. If you have got two Meta headsets (works with Quest 2, Pro, 3 and 3s), grab a friend, both flip on "WebXR experiments" in chrome://flags, open Shed Racer on the same table and turn on Shared Space in the lobby. The Quest setup guide has the details if your headset needs a nudge. And if you are building WebXR games: keep an eye on this one. It is one optional feature and a UUID, it falls back to nothing when it is missing and on the day it works it does something magical.

Written by

Florian Isikci

Founder, dmnshd

Florian has been shipping WebXR games and apps since 2018. He created Construct Arcade, the original WebXR game platform and has worked on titles like Hoverfit. Previously he ran the Vhite Rabbit (later Vhite Rabbit XR) studio. He founded dmnshd in 2026 to build a home for high-quality WebXR games that push the limits of the immersive web.

Ready to Play?

All games on dmnshd.gg are free and load instantly in your browser. A free account saves your progress and starts with 100 tokens.