A preview lays the scene out against the stage it is given: whatever the mount measures is the
Roblox viewport, mirrored onto Workspace.CurrentCamera.ViewportSize and fed to the layout engine.
That holds at every width and on every device, so a phone gets a phone-sized viewport and the scene
reflows into it exactly as the engine reflows there.
Nothing here needs configuration. The generated pages also size with dvh, so a full-height stage
does not hang underneath a mobile browser’s toolbars.
The stage is the viewport
Text re-wraps at the narrow width, a UIListLayout with Wraps re-flows onto more lines,
AutomaticSize re-measures, and a scene that branches on ViewportSize takes its narrow path. A
layout built out of offsets that is wider than the screen runs off the edge and is clipped — which is
what it does on a real device, and is usually the thing you opened the preview on a phone to find
out.
Roblox does not, so neither does loom. If a scene should adapt to a small screen, it adapts in the
scene — a UIScale, a breakpoint on ViewportSize, an AbsoluteSize binding. The preview’s job is
to show you what the engine would show, including when that is a layout hanging off the edge.
Why zooming is not the default
Until 0.10.2, a coarse-pointer device below 960px wide kept a 960-wide logical viewport and
painted the whole stage scaled down by hostWidth / 960. It read as the same layout drawn smaller.
It was not: it was a different layout, the one the engine gives at 960, shrunk to fit.
TextWrapped text kept the line breaks it had at 960 instead of re-wrapping, a UIListLayout with
Wraps kept its row count, AutomaticSize settled at the wide measurement, and scale-vs-offset
mixes re-proportioned against the wrong number. Everything looked fine — just small — and was wrong
in exactly the places a narrow viewport is the thing being checked.
(0.6.4 had already scoped the zoom off desktop for the same reason: an author dragging a window
narrow is asking to see the reflow. 0.10.2 finished the job.)
?base= keeps a wide composition
The zoom is still there, because a page embedding a preview at a fixed narrow width sometimes wants the composition rather than the reflow — a thumbnail of a wide dashboard in a docs column, say. It just has to be asked for:
| Value | Effect |
|---|---|
| absent | The default. The scene lays out against the real viewport at every width, on every device. |
?base=<px> | Keep this logical width. Below it the stage scales down to fit; at or above it nothing is applied. |
?base (bare) | The same, at 960. A value that cannot be read means this too — the param was typed to turn the zoom on. |
?base=none (or off, or 0) | The default, spelled out, for a host page templating the param. |
/loom-preview/index.html?target=src%2Fscenes%2FCounter.loom.tsx&chrome=none&base=1280960 is the bare-?base fallback rather than a constant with one right answer — the wider the base,
the more desktop layout survives intact and the smaller everything is drawn. It is wide enough that a
two-column or fixed-panel layout still has room, and small enough that a phone renders at roughly 40%
rather than the ~30% a 1280 base would give, where body text stops being readable.
With a base in effect the world still reads the mount’s untransformed layout size, so the scene sees the logical viewport and you are previewing the wide layout, drawn smaller. The debug panel shows both boxes and the factor between them, which is how you tell a scene that laid out small from one that was painted small.
?base= sits alongside the rest of the URL
contract.
Pointer coordinates follow the scale
Whenever ?base= is scaling the stage, an on-screen pixel is not a layout pixel. The renderer
converts back by reading the mount’s own rendered-to-layout ratio, so hit testing lands where the
scene looks like it is. That covers:
MouseEnter/Activated/InputChangedpositionsGetMouseLocation- Wheel deltas
Unscaled — the default — the ratio is 1 and the conversion is a no-op.
ScrollingFrames scroll from a touch drag
There is no wheel on a phone. A drag inside a ScrollingFrame moves CanvasPosition with the same
clamping the wheel path uses, and past a small slop threshold it stops counting as a tap — so
dragging past a button does not activate the button under your finger.
Two deliberate limits:
- Only
ScrollingFrames opt out of native touch panning. A preview embedded in a docs page never traps the reader’s scroll; the page keeps scrolling normally everywhere else. - Taps do not wait for the double-tap-zoom timeout. A tap registers immediately rather than after the browser’s ~300ms delay.
The gallery chrome stacks
In full-chrome gallery mode, the 248px sidebar becomes a top bar with a targets button. Opening it
shows the target list; picking one closes it again, leaving the rest of the screen to the stage.
?chrome=none — what the docs-site
iframes use — is unaffected, since there is no chrome to
stack. Embedded previews get the touch handling and nothing else.