Loom is a development tool. It renders your UI tree in a browser so you can iterate without a Studio round-trip. Nothing it produces ships to Roblox, and its fidelity is deliberately partial.
A preview is a high-fidelity approximation of Roblox layout and painting, not a reimplementation of the engine. Use it to iterate on structure, sizing, state and interaction. Verify final visuals in Studio before shipping.
Implemented
| Area | Status |
|---|---|
| Scene IR + WASM layout + DOM renderer | Complete — the vertical slice all frontends share. |
Roblox datatypes, Instance model, signals, services, scheduler | Complete enough to run real component libraries. Attributes since 0.11.0. |
| Luau standard library | Complete as of 0.8.0 — table, string, math, os, bit32, utf8, debug, buffer and the free functions, installed before your entry. Metatables are the deliberate omission. |
@rbxts/react adapter | Complete: a custom react-reconciler host config driving live loom instances. |
Layout: list, grid, table, pages, padding, constraints, AutomaticSize, text measurement | Complete — every UIGridStyleLayout the engine has, as of 0.7.0. |
Visuals: text, UICorner, UIStroke, UIShadow, gradients, clipping, transparency, rotation, scrolling frames | Complete. |
Images: every ScaleType, 9-slice, tiling, sprite windows, tints, rbxassetid:// | Complete as of 0.7.0, including asset ids in a static build — ids composed at runtime since 0.9.5. |
Text: FontFace, RichText markup, wrapping, LineHeight and measurement | Complete, and calibrated against the engine as of 0.9.5 — TextSize as a face height, half-pixel advances, kerning, one wrap authority for measurement and paint. |
Fonts: the engine’s typefaces, registerFont | The preview loads 28 of Roblox’s families as of 0.9.0, and names the rest; unbacked families warn, and since 0.9.6 a registered face that never arrived warns too. |
Scrolling: ScrollingFrame, canvas growth, scroll bars | Complete as of 0.9.3 — before it, a canvas never outgrew its window and no bar was drawn. |
| Debug panel | As of 0.10.0 — timings, viewport, live tree, loaded typefaces, and a stage inspector. Off unless asked for. |
Flex: UIListLayout flex and Wraps, UIFlexItem | Complete. |
TweenService, TextService, Debris, bindings, @rbxts/ripple | Complete. |
Pointer and keyboard input, focus, UserInputService | Complete, with Roblox input-sinking semantics. |
Dev loop: Vite plugin, HMR, loom preview, loom build | Complete. |
vide adapter | Shipped, on the same Scene IR. |
Those rows are area-level. For the per-property answer — is AutomaticCanvasSize honored, does
UIGradient.Rotation work, what happens to ScrollBarThickness — see Supported instances and
properties.
The practical proof point: a full headless component library — dialogs, popovers, comboboxes,
selects, tabs, sliders, focus scopes, portals and layers — renders and behaves correctly under loom preview, across more than fifty scenes, without a single source change on the library’s side.
Not implemented
UIScale. A valid JSX intrinsic with typed props, and a no-op: nothing reads UIScale.Scale, so
a scaled subtree renders at 1×. (UIFlexItem, which used to sit in this row, is implemented as of
0.5.0.)
TextScaled. Accepted on every text class and never applied — text renders at TextSize
whether or not it fits.
Gotham and BuilderSans, out of the box. Roblox’s default family, and the Builder faces behind
it today, are proprietary and cannot be redistributed, so the preview loads only the openly licensed
families. Both are still named, so they resolve to a stack and warn rather than drifting silently,
and a project that has the files registers them itself — see Fonts and text
metrics.
Metatables. setmetatable / getmetatable / newproxy are not installed. Loom runs the
author’s TypeScript, whose classes are JavaScript classes, and there is no faithful way to give a
plain object a metatable’s __index behaviour without proxying every table in the program.
A roblox-ts compiler transform. The preview path deliberately skips rbxtsc and lets esbuild
transpile. A standalone transform, which would let loom consume compiled output rather than source,
is still open.
A Luau adapter. The IR was designed so a Luau frontend can be added. It has not been.
Things that are true, not bugs
- A list can flow alphabetically.
SortOrderdefaults toEnum.SortOrder.Nameon every layout, as it does in Studio, so children with distinctNames sort by name rather than by source order. Loom defaulted toLayoutOrderuntil0.7.0; a list that reordered on upgrade was already ordering that way in the engine. SetSortOrder={Enum.SortOrder.LayoutOrder}for the old behaviour. - No typechecking. esbuild strips types; a preview renders whether or not your project typechecks. See TypeScript setup.
printtypes oddly. The globals declaration deliberately omitsprintbecauselib.domalready declares one and redeclaring is a compile error. The runtime still overwrites the value.- Unknown services are stubs.
@rbxts/servicesis aliased to a module exporting only the services the runtime actually implements — the behavioural ones (CollectionService,ContextActionService,Debris,GuiService,HttpService,Players,RunService,StarterGui,TextService,TweenService,UserInputService,Workspace) plus the container-only ones (ReplicatedStorage,Lighting,SoundService, …). Importing anything else is a build error rather than a silent stub, which is the intended trade. - A Luau-only package needs a shim. Most roblox-ts packages redirect to their own TypeScript
source automatically; a declaration-only one has no source to redirect to and fails by name until
you point
shimsat a browser module.@rbxts/react,@rbxts/react-roblox,@rbxts/services,@rbxts/vide,@rbxts/ui-labs,@rbxts/rippleand@rbxts/react-rippleare handled out of the box. rbxassetid://is resolved for you, twice over. Roblox’s thumbnail API sends no CORS headers, so a dev-server route resolves the id underloom preview, the embedded server and Next dev. A static build has no server to ask, so as of0.7.0the ids are resolved at build time and the images are written into the output — read out of the bundle, and, since0.9.5, by mounting each gallery target and reading the tree, which is what finds an id composed at runtime. An image the first render never reaches still needs a real URL, or your ownsetImageResolver.- The React version is pinned in effect. The adapter’s reconciler targets React 18; the plugin
aliases bare
reactto one absolute path to guarantee a single instance. - Text metrics are the browser’s, corrected toward the engine’s. Loom measures with canvas
measureText(), so shaping and hinting are not Roblox’s. Three things narrow the gap: the preview loads the right typeface itself as of0.9.0,TextSizeis divided by the face box rather than spent as afont-sizeas of0.9.3, and advances are quantized to the half pixel and kerned the way the engine spends them as of0.9.4–0.9.5. AgainstGetTextBoundsAsyncat 50 widths, the wrapped line count now matches 49 times; what is left is a measurement about a percent roomy, so text wraps a hair early rather than overflowing. The detail.
Versioning
All @loom-dev/* packages release in lockstep — the changesets configuration marks them fixed, so
a bump to one bumps them all. Mismatched versions across the scope are never a supported combination.
The project is pre-1.0: expect the plugin’s config contributions and the gallery URL contract to be
the stable-ish parts, and internal package boundaries to move.