Loomgetting started

Scope and status

What loom renders faithfully, what it recognizes but ignores, and what it will never be — read this before trusting a preview.

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.

Implemented

AreaStatus
Scene IR + WASM layout + DOM rendererComplete — the vertical slice all frontends share.
Roblox datatypes, Instance model, signals, services, schedulerComplete enough to run real component libraries. Attributes since 0.11.0.
Luau standard libraryComplete as of 0.8.0table, string, math, os, bit32, utf8, debug, buffer and the free functions, installed before your entry. Metatables are the deliberate omission.
@rbxts/react adapterComplete: a custom react-reconciler host config driving live loom instances.
Layout: list, grid, table, pages, padding, constraints, AutomaticSize, text measurementComplete — every UIGridStyleLayout the engine has, as of 0.7.0.
Visuals: text, UICorner, UIStroke, UIShadow, gradients, clipping, transparency, rotation, scrolling framesComplete.
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 measurementComplete, and calibrated against the engine as of 0.9.5TextSize as a face height, half-pixel advances, kerning, one wrap authority for measurement and paint.
Fonts: the engine’s typefaces, registerFontThe 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 barsComplete as of 0.9.3 — before it, a canvas never outgrew its window and no bar was drawn.
Debug panelAs of 0.10.0 — timings, viewport, live tree, loaded typefaces, and a stage inspector. Off unless asked for.
Flex: UIListLayout flex and Wraps, UIFlexItemComplete.
TweenService, TextService, Debris, bindings, @rbxts/rippleComplete.
Pointer and keyboard input, focus, UserInputServiceComplete, with Roblox input-sinking semantics.
Dev loop: Vite plugin, HMR, loom preview, loom buildComplete.
vide adapterShipped, 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. SortOrder defaults to Enum.SortOrder.Name on every layout, as it does in Studio, so children with distinct Names sort by name rather than by source order. Loom defaulted to LayoutOrder until 0.7.0; a list that reordered on upgrade was already ordering that way in the engine. Set SortOrder={Enum.SortOrder.LayoutOrder} for the old behaviour.
  • No typechecking. esbuild strips types; a preview renders whether or not your project typechecks. See TypeScript setup.
  • print types oddly. The globals declaration deliberately omits print because lib.dom already declares one and redeclaring is a compile error. The runtime still overwrites the value.
  • Unknown services are stubs. @rbxts/services is 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 shims at a browser module. @rbxts/react, @rbxts/react-roblox, @rbxts/services, @rbxts/vide, @rbxts/ui-labs, @rbxts/ripple and @rbxts/react-ripple are 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 under loom preview, the embedded server and Next dev. A static build has no server to ask, so as of 0.7.0 the ids are resolved at build time and the images are written into the output — read out of the bundle, and, since 0.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 own setImageResolver.
  • The React version is pinned in effect. The adapter’s reconciler targets React 18; the plugin aliases bare react to 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 of 0.9.0, TextSize is divided by the face box rather than spent as a font-size as of 0.9.3, and advances are quantized to the half pixel and kerned the way the engine spends them as of 0.9.40.9.5. Against GetTextBoundsAsync at 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.