Mapping CSS Box Models and Grid Layouts to 3D Coordinates
A 3D layout engine for the web does not need to reimplement CSS — it needs a lawful way to read what CSS decided. The working pattern is to lay the author's own stylesheet out over an offscreen tree of empty stand-in boxes, one per element being laid out and each carrying that element's declared size, measure the result with offset geometry, and map each measured rectangle to a world-space pose through a fixed projection contract of 50 CSS px per world unit. A grid template written for a 2D page then positions boxes in a 3D volume, with the browser's real layout engine doing all of the layout.
The things being positioned are elements of the Scene Document Model: 3D volumes, not HTML elements. A slab being laid out can be a solid body, a bare edge frame, or an empty seat that holds a place in the layout with nothing in it. None of them has a DOM box for CSS to lay out, and writing a bespoke constraint solver to replicate CSS grid, flexbox, margin collapsing, and intrinsic sizing would mean maintaining a second layout engine that can only drift from the browser's. The engine instead derives layout from a stand-in. CSS solves boxes; a solver poses volumes. The reference implementation throughout is the Software 4 Senses 3D Spatial Web Studio ("the Studio" below).
Measurement twin — an engine-derived, never-authored offscreen DOM container holding one empty stand-in box per element being laid out, each carrying that element's declared size, laid out by the author's verbatim CSS and measured with offset geometry. The elements themselves are records in the scene document, not DOM; the twin exists solely so the browser's own layout engine can lay out boxes on their behalf.
Two things the twin is not: it is neither the platform's Shadow DOM nor a hidden state store. It is an offscreen layout-measurement container — CSS lays it out so a pure solver can pose 3D elements, and that is its entire job. State for 3D elements lives with the elements themselves (their records, animation clips, and state machines, whose definitions the scene document saves), never in any hidden DOM.
The measurement-twin pattern
The measurement twin lets real author CSS lay out elements that have no DOM box of their own, by laying out stand-ins for them instead. The Studio's shipped procedure, end to end:
- Give each element an empty stand-in box. The stand-in is a plain
<div>carrying the element's declared size in CSS px as its width and height — or, for a slab set to adopt its cell, as a minimum width and height, so the stand-in stretches to fill the cell the layout grants it, and overflows that cell if its declared size is larger. - Seat the stand-ins in an offscreen container laid out by the author's own CSS.
The stylesheet is applied verbatim and unscoped, with one exception: a bare
[data-rig]selector is bound to this rig when the sheet is written, so a layout rule can scope itself without naming the rig. Each stand-in sits inside a persistent, engine-owned cell box. The engine's own stylesheet makes exactly one declaration about that cell (display: grid, placed first in the sheet so an author rule of equal specificity still wins), and beyond its id the only thing ever written on a cell is a seat size the author states for it in the rig definition. Nothing derived — row, column, or a mirrored class — is ever written onto a cell. - Measure with offset geometry. The twin reads
offsetLeft/offsetTop/offsetWidth/offsetHeight, summing offsets up theoffsetParentchain to its own container, so every rect is container-relative. - A pure solver maps each rect to a world pose. The solver takes measured CSS rects plus the rig definition and returns world-space poses. It imports no DOM and no engine — given the same rects it returns the same poses, which is what makes the layout half testable without a GPU.
- Arrangement changes tween FLIP-style from live poses. When the layout
changes (a grid becomes a carousel), the target layout is solved instantly and each element is
animated from its current live pose to its target as one interruptible group — the classic
First-Last-Invert-Play technique lifted into world space. The Studio's shipped default for
these transitions is 450 ms with
cubic-bezier(0.33, 1, 0.68, 1)— a designed, feel-tested default, not a measurement.
One stability rule rides on top: nothing derived from a measurement — no row, no column, no mirrored class — is ever written back onto a cell, so an author rule keyed on a measured value can never resize a cell, reflow the rig, and re-enter its own input; the measure → pose → measure feedback loop is structurally impossible rather than merely damped.
The projection contract
Three fixed conventions connect a measured CSS rectangle to a world-space pose. All three are contracts the engine holds itself to — conventions, not measurements:
- 50 CSS px = 1 world unit. Exported as
PX_PER_WORLD = 50; the code comment calls it a "contract constant, not folklore." The same knob has a second spelling on the record side: a slab's edge frame and solid body are sized to a world half-extent ofwidthPx/cssPerspective(the body less its edge rounding), and the two spellings are locked by the identitycssPerspective = 2·sampleScale/pxScale(100 at sample scale 1). Since a ruling,1/50is the single source for every slab, standalone or rig-owned; before that, one preset stated the equivalent of 1/40, making the same element definition 1.25× larger in world space off-rig than on it. - The field of view is the HALF-angle of the canvas's shorter side. At depth
d that side spans
2·d·tan(fov)— notd·tan(fov)and not2·d·tan(fov/2)— so a landscape view's height, and a portrait view's width, is2·d·tan(fov). Depth here is the component of (element − eye) along the view direction, not Euclidean distance: an off-axis element at the same depth subtends the same height. - World +y projects DOWN on screen. The vertical axis follows the DOM, not math-textbook convention: "down the page" is +y travel, and "above" means negative y. Scroll choreography, layout math, and authored content then share one mental model instead of negating y at every boundary.
Pitfalls measured in the field
Two failure modes were hit and probed while shipping this pattern; each dictates one of the twin's design choices.
display: nonecollapses every offset rect to zero. An offscreen twin cannot simply be hidden the obvious way: adisplay: nonesubtree has no boxes, so every offset read returns 0. The twin instead parks atleft: -100000pxwithvisibility: hidden,pointer-events: none, andaria-hidden="true"— laid out and measurable, but invisible, unclickable, and silent to assistive technology.- Per-item grid placement does not resolve. In the Chromium builds probed
during development, asking a grid item for its resolved placement (the
grid-row-startroute) is empirically dead — auto-placed items do not report the line they landed on. Rects alone say where a cell landed but not which grid line it belongs to, so the twin instead harvests the container's used track list and alignment viagetComputedStyle, and a pure solver rebuilds the grid-line edges from the tracks.
In the Studio
In the Studio this machinery surfaces as the slab/rig layer: a slab is the atomic content-bearing volume of a 3D site (a card in the Studio's vocabulary) — the div's replacement, which may carry a solid body and an edge frame, each optional and neither present by default — and a rig is an arrangement machine (plane, shelf, ring, stack) that positions slabs, the 3D analogue of a flex or grid container, driven by real author CSS through the measurement twin described above. Within the Scene Document Model the rig is the layout machine, and its layout cells are first-class: minted once with ids that are never recycled, cells outlive their occupants — a slab can vacate its seat and another can be seated in the same cell without the layout being rebuilt. The definition layer is guarded by a validator in which unknown keys throw at every level rather than being silently ignored — a typo is an error message, not a field that does nothing — and normalized output revalidates to itself, so a saved scene round-trips through the same schema that authored it. That schema discipline is what keeps a layout system spread across CSS, stand-ins, solvers, and GPU records debuggable: every layer either accepts a definition exactly or says precisely why not.
Related reading
- What Is a 3D Spatial Web Interface? — the category definition this layout mechanism belongs to.
- The Scene Document Model: Document Structure for 3D Elements — the model whose elements this page's layout machinery positions: 3D volumes with document structure, addressed and animated as records rather than HTML.
- Scroll Semantics for 3D Sites: Rails, Detents, and Scroll Grammars — what scrolling means once layout lives in a 3D volume.
- Migrating a 2D HTML/CSS Layout to a 3D Spatial Web Interface — the walkthrough that puts this page's math to work on an existing site.