Small, copy-pasteable pieces for building worlds out of plain HTML. Every recipe works in a file that starts with the starter template, and each one links to a world that uses it. For the full reference (and for your AI agent) see llms.txt.
One script tag makes the page a world. Everything in <body> is an object, placed with a CSS transform in centimeters. y is up, and you face −z when you arrive.
On flat terrain the ground is at 75cm, so eye level is about 235cm.
<html> <head> <script src="https://webspaces.space/run/0.10.0-alpha.11/webspace.js"></script> <meta name="webspace.environment.terrain.type" content="flat" /> </head> <body> <label id="hello" style="transform: translate3d(0cm, 235cm, -300cm)"> <h1>Hello</h1> </label> <img src="photo.jpg" style="transform: translate3d(200cm, 235cm, -300cm) rotateY(-0.5rad)"> </body> </html>
Scan something with a phone app (Scaniverse, Polycam, Luma…) and export .spz or .ply. It's one tag. Splats keep real-world scale.
Most .ply captures are upside down. Convert and tidy them with splat2spz.mjs (--flip --center), which also makes them about 10× smaller.
<!-- a captured object, forty times life size --> <model id="cactus" src="cactus.spz" style="transform: translate3d(0cm, 75cm, -500cm) scale3d(4, 4, 4)"></model> // convert a capture node splat2spz.mjs scan.ply scan.spz --flip --center
Any equirectangular (360°) photo can be the sky: phone panoramas, 360 cameras, or generated skyboxes.
Poly Haven has hundreds of CC0 skies. Download the tonemapped JPG, resize it to 4096×2048, and prefer the "puresky" variants. Turn fog off to see all of it.
<meta name="webspace.environment.sky" content="sky.jpg" /> <meta name="webspace.environment.fog" content="off" />
A dark sky color makes night: the sky darkens and the world's light turns soft. Set the rest of the palette dark too. Things that glow (splats with additive blending) then carry the scene.
Far-away things, like a galaxy overhead, also need fog and wrap off.
<meta name="webspace.environment.terrain.colors.sky" content="#05060f" /> <meta name="webspace.environment.terrain.colors.ground" content="#10162b" /> <meta name="webspace.environment.fog" content="off" /> <meta name="webspace.environment.wrap" content="off" />
In-world clicks (mouse, touch, or a VR controller) arrive as ordinary click events. Put what happened into webspace.state, then react in a change listener, so everyone sees it, including people who arrive later.
<script type="module"> await webspace.ready; const bell = document.getElementById("bell"); bell.addEventListener("click", () => webspace.state.set("rung", Date.now())); webspace.state.addEventListener("change", e => { if (e.detail.key === "rung") ring(); // runs for everyone }); </script>
Set style.transform every frame. Driving motion from Date.now() keeps it in step for everyone with no networking at all. Any CSS transform works, including rotateY() and scale3d().
Changes made by scripts are never saved into the file, so it's safe to animate everything.
const moon = document.getElementById("moon");
(function orbit() {
const a = (Date.now() / 20000) % (2 * Math.PI);
moon.style.transform =
`translate3d(${Math.cos(a) * 600}cm, 500cm, ${Math.sin(a) * 600}cm)`;
requestAnimationFrame(orbit);
})();
SVOX is plain text. Each line is a z slice, each space-separated group is a y layer (bottom to top), and each character is an x voxel. - is empty; letters are colors. deform smooths the blocks into soft shapes.
Save it as stone.svox, then use <model src="stone.svox">.
size = 5 3 5 scale = 0.09 origin = -y material lighting = smooth, roughness = 1, deform = 5 colors = D:#6f6a63 E:#57524c voxels -DDE- -DED- ----- DDEDD DEDDE -DDE- DEDDE EDDED -DED- EDDED DDEDD -EDD- -DED- -EDD- -----
Labels and emoji objects update from scripts like any element: set innerHTML on text, or textContent on an emoji.
document.getElementById("score").innerHTML = "<h1>Score: 5</h1>"; document.getElementById("bud").textContent = "🌷";
Splats don't have to be scans; they can be light. With mix-blend-mode: plus-lighter, overlapping splats add up like real light, which suits fire, stars, magic, and nebulae. opacity fades them.
Generate them with the ~100-line spz-writer.mjs.
<model id="flame" src="flame.spz" style="mix-blend-mode: plus-lighter; transform: translate3d(0cm, 85cm, -250cm)"></model> // in node: one splat encodeSpz([{ x: 0, y: 1, z: 0, sx: .1, sy: .1, sz: .1, r: 1, g: .6, b: .2, a: .5 }]);
Every world works in headset browsers. An Enter VR button appears when there's a headset. Thumbsticks walk and turn, and trigger or pinch clicks arrive as the same click events, so one script serves every device.
if (webspace.xr.isPresenting) { /* in a headset */ }
// enter from your own button (must be a click handler)
button.addEventListener("click", () => webspace.xr.enterVR());
A webspace is static files. Keep webspace.service.1.0.1.js next to each world's HTML and host it anywhere. With GitHub Pages, the world can also save in-world edits back to the repo.
# the fastest way
1. github.com/webspace-sdk/start → Use this template
2. Settings → Pages → Deploy from main
3. open https://<you>.github.io/<repo>/