Loomreference

Supported instances and properties

The per-property support matrix — what loom renders, what it accepts and silently ignores, and what will not typecheck at all.

Scope and status answers “is text done?” at the area level. This page answers “does AutomaticCanvasSize work?” at the property level, so you know exactly how far a preview can be trusted before you open Studio.

Everything below describes loom 0.11.0: the JSX intrinsics in @loom-dev/react, the DOM mapping in @loom-dev/renderer, and the Rust layout engine in crates/loom-layout.

How to read this

MarkMeaning
SupportedRead by the layout engine or painted by the renderer. Behaves like Roblox, within the fidelity limits below.
PartialImplemented, with a stated gap. Correct in the common case, wrong at the edge.
AcceptedTyped and settable, and nothing reads it. No error, no effect.
Not typedNo JSX prop and no runtime handling. A compile error, and it would not render either.

Accepted is the row that costs you time, so it is spelled out rather than omitted. Those props exist so real component code — which sets them for the Roblox build — runs under the preview without edits.

Instances

IntrinsicStatusNotes
screengui surfacegui billboardguiSupportedLayerCollectors: never background-painted, always click-through, z-ordered among themselves by DisplayOrder.
frameSupported
scrollingframeSupportedSinks pointer input unconditionally, like Roblox.
canvasgroupSupportedGroupTransparency becomes CSS opacity on the container div, which is the same “composite the subtree, then fade it” model.
textlabel textbutton textboxSupportedSee Text.
imagelabel imagebuttonSupportedThe Image paints in a layer beneath the text, in every ScaleType. See Image properties.
viewportframe videoframePartialTyped and laid out as plain GuiObjects. No 3D scene, no video — they paint as backgrounds.
uilistlayout uigridlayout uipaddingSupported
uipagelayout uitablelayoutSupportedImplemented in 0.7.0. See Layout and modifier properties.
uicorner uistroke uigradientSupportedMapped to border-radius (per-corner), a box-shadow ring on the side BorderStrokePosition asks for, and a CSS linear-gradient.
uishadowSupportedA CSS drop shadow, layered under a UIStroke ring rather than replacing it.
uisizeconstraint uiaspectratioconstraintSupported
uiscaleAcceptedTyped, recognized as a modifier, and read by nothing. A scaled subtree renders at 1×.
uiflexitemSupportedFlexModeGrow / Fill, or Custom with GrowRatio.
Everything elseNot typeduitextsizeconstraint, folder, part, … — the intrinsic set is exactly what the renderer implements.

GuiObject properties

Shared by every visual class.

PropertyStatusNotes
Size Position AnchorPointSupportedFull UDim2 scale + offset resolution.
AutomaticSizeSupportedX / Y / XY grow-to-content, with the object’s own Size as the floor and the room its parent leaves as the ceiling — see below.
BackgroundColor3 BackgroundTransparencySupported
VisibleSupportedHidden via CSS; the node still occupies its computed rect, as in Roblox.
ZIndexSupportedZIndexBehavior.Sibling semantics — the renderer’s native model.
LayoutOrderSupportedHonored by every layout under SortOrder.LayoutOrder — which is not the default. See SortOrder defaults to Name.
RotationSupportedDegrees clockwise about the element center, as a CSS transform. Purely visual: the layout rect is unrotated, matching Roblox.
ClipsDescendantsSupported
ActivePartialHonored at runtime, not declared in the props type, so setting it works and your editor will redline it. It governs whether input is sunk, not whether the object hears it: a frame with an InputBegan handler is hit-testable either way, and a frame with no listeners stays click-through.
NameSupported
(unwritten properties)SupportedReading a property nobody has written yields the Roblox default — Visible, ZIndex, BackgroundTransparency, Rotation, LayoutOrder, Active, ClipsDescendants, AnchorPoint, Position, Size — not undefined.
Event Change refSupportedEvent={{ Activated }}, Change={{ Text }}, and ref to the live LoomInstance.
BorderSizePixel BorderColor3 SizeConstraint Transparency InteractableNot typed

ScreenGui properties

PropertyStatusNotes
DisplayOrderSupportedBecomes the layer’s CSS z-index (may be negative).
EnabledSupported
ZIndexBehaviorAcceptedSibling is what the renderer already does; Global is not emulated.
IgnoreGuiInsetAcceptedThe runtime’s GetGuiInset() is zero, so there is no inset to ignore.
ResetOnSpawn ScreenInsetsAcceptedTyped so real code runs unchanged; nothing reads them.

Text properties

PropertyStatusNotes
Text TextColor3 TextSize TextTransparencySupportedA newline in Text breaks the line and a run of spaces stays a run of spaces, as in Roblox — since 0.9.3, when the paint stopped folding both away through HTML’s defaults.
TextXAlignment TextYAlignmentSupported
TextWrappedSupportedwhite-space: normal vs nowrap, and applied during measurement — see below. The deprecated TextWrap alias reads as the same property and is declared on the props type as of 0.6.3.
LineHeightSupportedThe multiplier, clamped to the 1…3 Studio allows and spent between lines: n lines measure TextSize + (n - 1) * TextSize * LineHeight, so a one-line label is exactly TextSize tall however high its LineHeight. The browser’s taller line boxes are handled by the clip rect rather than by cropping the glyphs — see below.
FontFaceSupportednew Font(family, weight, style) resolved to a CSS family/weight/slant, including for text measurement. Preferred over the legacy Font enum when both are set.
FontSupportedThe legacy enum — all 53 items in the engine’s own order as of 0.9.0 — mapped to a family by name prefix, with the weight read off the name suffix. 28 families load a real face; the eight the engine licenses privately (Gotham, BuilderSans, Bodoni, …) resolve to a stack and warn. See Fonts and text metrics.
FontSizeSupportedThe legacy Enum.FontSize — the pixel size is read out of the enum name, so Size24 paints and measures at 24px. TextSize wins when both are set, as in Roblox.
RichTextSupported<b>, <i>, <u>, <s>, <br/>, <font> (colour, size, face, family, weight, transparency), <uppercase> / <smallcaps> and the character entities. With the flag off — or on a tag the engine would not recognise either — the markup stays literal, as in Roblox. Measurement uses each run’s own font.
TextScaledAcceptedNever applied. Text renders at TextSize whether or not it fits its box.
TextTruncate MaxVisibleGraphemes TextStrokeColor3 TextStrokeTransparencyNot typedNo ellipsis, no glyph outline.

AutomaticSize on text

Auto-sizing text is measured browser-side — the adapter runs a canvas measureText() with the same font the renderer paints and hands the result to the WASM engine as TextBounds, because font metrics do not exist inside the engine. Which typeface that is, and why it is worth pinning, is Fonts and text metrics.

TextWrapped is honored: the runs are laid into lines at word boundaries rather than measured as one long line. Since 0.9.5 one function decides every wrap — measurement asks it how many lines a label needs, and the text layer asks it where to put the breaks it paints. Before that the box came from the measurer while the glyphs inside it were left to CSS, which wraps on its own kerned run widths, so a label could reserve nine lines and paint eight, breaking at different words than Studio. The advances behind it are the engine’s: half-pixel per grapheme since 0.9.4, plus the run’s kerning since 0.9.5.

The wrap width is the nearest ancestor that has a width of its own, less every UIPadding in between — not the immediate parent. A parent that is itself AutomaticSize was sized by the label, so wrapping against it is the same circle as wrapping against the label’s own width, and the text never wraps at all. The library idiom stacks two or three such containers (a padded body inside a flex item inside a card), and the card — the one node with a real width — is where the room actually runs out.

The re-wrap settles inside the paint that caused it. The wrap width comes from the layout that paint produces, so the first measurement after a container narrows is still against the old width; the adapter re-encodes and re-lays-out until the two agree (up to four passes, then it defers to the next frame) and patches the DOM once, with the settled result. Rendering the first pass would put a label wider than its container on screen for a frame — which, during a live window drag, is every frame.

A RichText string is measured run by run in the font its own tags ask for, so a bold or resized run does not clip. An empty TextBox is measured against its PlaceholderText, which is what it displays — otherwise an AutomaticSize.Y input would collapse to zero height and become unclickable.

Both adapters do this. The vide adapter measured by splitting on newlines only until 0.6.4, so TextWrapped did nothing there and the same scene laid out differently through vide than through react — the one thing a shared Scene IR is supposed to rule out.

The wrap width also carries the UIPadding insets in between, including scale ones: the layout engine resolves a scale inset against the node’s own width where its X axis is a real one, and against zero where the axis is automatic (a scale inset on an automatic axis is circular — the width sets the padding sets the width). Until 0.8.1 the adapter read offsets only, which is the right answer for the automatic case and wrong for the other, so a wrapped label under a PaddingLeft={new UDim(0.15, 0)} ancestor was measured against a width that ignored the inset and came out with a box built for fewer lines than it was then painted with.

TextSize is a face height, not a font size

TextSize means different things to the two renderers. Roblox fits the whole face into it — ascender to descender, which is why one line measures exactly that tall — while CSS font-size sets the em square, and a face’s ascent plus descent runs well past 1em.

Until 0.9.3 loom painted font-size: TextSize straight through, so every glyph came out too big by its own face’s ratio: 17% for Roboto, 25% for Merriweather, 47% for Oswald. Text then measured that much wider, wrapped that much earlier, and AutomaticSize boxes came out taller and wider than the engine’s. Loom now divides by the face box, and LineHeight sets the line box in pixels off TextSize because the pitch the engine spends is TextSize-relative rather than font-size relative. Which ratio to divide by is itself calibrated per family — see TextSize is not a font size.

Until 0.8.1 the text overlay was also clipped to the box the layout computed, so the browser’s taller line boxes had nowhere to go and the clip took the difference out of the glyphs — a paragraph’s last line lost the tails of its y, p and g, and at a large TextSize the first line lost the tops of its ascenders. The clip rect carries that overhang now, with padding handing the content box its original height straight back. Nothing about the layout moves: TextBounds, AbsoluteSize and every rect around the label are the values they were, and a label still clips its own text at its left and right edges.

AutomaticSize is bounded by the parent

Roblox bounds automatic growth: an object with AutomaticSize on an axis grows up to the maximum size its parent allows, and a TextWrapped label grows until that maximum and only then wraps. Until 0.6.4 loom grew unbounded, so any content with an irreducible minimum — a row of buttons, a long word — pushed its container past the slot positioning it, and at a narrow viewport a 45%-wide card grew over the card beside it.

Every auto-sized node now carries the ceiling its parent leaves, inherited through the padding in between. Size remains the floor even when it is itself past that ceiling, and a wrapping UIListLayout on an automatic fill axis wraps against the same ceiling rather than measuring as one run. Content that still does not fit overflows, which is what the engine does — it does not widen the object to make room.

A ScrollingFrame is the exception, since 0.9.3. A canvas free to grow is one whose entire point is outgrowing the window, so children of a ScrollingFrame get no ceiling on an axis carrying AutomaticCanvasSize or a CanvasSize of 0. A CanvasSize that gives the axis a real extent is still the ceiling it always was. Before that release the ceiling applied here too, so the canvas came out exactly the window’s size and nothing ever scrolled.

Image properties

Shared by imagelabel and imagebutton. The image paints in a layer beneath the text — a background-painted element rather than an <img>, because a sprite window and a 9-slice both place a region of the source, which object-fit cannot express.

PropertyStatusNotes
ImageSupportedhttp(s):, data: and blob: URLs load directly. rbxassetid:// is resolved by a dev-server route, and baked into a static build — see the callout below.
ImageTransparencySupported
ImageColor3SupportedA per-channel multiply through an feColorMatrix, matching the engine — so a full-colour image tints as correctly as a monochrome icon. The default white costs no filter at all.
ScaleTypeSupportedAll five as of 0.7.0: Stretch, Fit, Crop, Slice and Tile.
SliceCenter SliceScaleSupportedThe 9-slice centre in the source image’s own pixels, painted as a CSS border-image: corners keep their size, edges stretch along one axis, the middle along both. SliceScale scales the painted border without touching the source. A SliceCenter that leaves no centre (the default Rect.new(0, 0, 0, 0) included) stretches instead.
TileSizeSupportedOne tile’s size for ScaleType.Tile, a UDim2 against the node — so UDim2.fromScale(0.5, 0.5) is four tiles. Defaults to {1,0},{1,0}, one tile filling the node.
ImageRectOffset ImageRectSizeSupportedThe sprite window, in image pixels. A zero ImageRectSize means the whole image, as in Roblox. The window is clipped to its own destination rect, so a sheet’s neighbouring sprites never leak under Fit.
ResampleModeSupportedPixelated becomes CSS image-rendering: pixelated — pixel art scales up crisp instead of blurred.

Two combinations are not reproduced, and say so rather than painting something wrong:

  • ScaleType.Tile with an ImageRect window. CSS backgrounds cannot repeat a region, so the whole image tiles and loom warns once, naming the node.
  • A SliceCenter measured against a window rather than against the whole image. The slice insets are read from the source’s natural size.

TextBox properties

PropertyStatusNotes
PlaceholderTextSupportedBecomes the DOM input’s placeholder.
ClearTextOnFocus TextEditable MultiLineSupported
PlaceholderColor3AcceptedThe placeholder is not restyled — you get the browser default.

ScrollingFrame properties

PropertyStatusNotes
CanvasSizeSupportedChildren lay out against the canvas, not the window.
CanvasPositionSupportedApplied as the DOM scroll offset by the renderer, not by the layout engine.
AutomaticCanvasSizeSupportedX / Y / XY. The affected axis grows to the union bounding box of the frame’s direct children, then max()ed with the resolved CanvasSize.
ScrollingDirection ScrollingEnabledSupportedDecide which axes can scroll, and therefore which bars can appear.
ScrollBarThicknessSupportedThe bar’s width, default 12. A thickness of 0 shows no bar, as in Roblox.
ScrollBarImageColor3SupportedTints the thumb. Untinted, loom draws the engine’s grey rather than the property’s own white default — the engine tints grey sprites loom does not fetch, and a white bar would be invisible on most surfaces.
ScrollBarImageTransparencyAcceptedThe thumb is drawn opaque.
VerticalScrollBarInset HorizontalScrollBarInsetAcceptedThe bar is always painted over the canvas — Roblox’s ScrollBarInset.None, which is also the engine’s default. AbsoluteWindowSize reserves nothing for it, so a layout that budgets for an inset bar comes out that many pixels wider here than in Studio.

Layout and modifier properties

InstanceSupportedAccepted / partial
uilistlayoutFillDirection, HorizontalAlignment, VerticalAlignment, SortOrder, Padding, HorizontalFlex, VerticalFlex, Wraps
uigridlayoutCellSize, CellPadding, FillDirection, FillDirectionMaxCells, HorizontalAlignment, VerticalAlignment, SortOrderStartCorner — only TopLeft is implemented; the other three corners fall back to it.
uitablelayoutMajorAxis, Padding, FillEmptySpaceColumns, FillEmptySpaceRows, HorizontalAlignment, VerticalAlignment, SortOrderSee UITableLayout.
uipagelayoutFillDirection, Padding, Circular, HorizontalAlignment, VerticalAlignment, SortOrderAnimated, TweenTime, EasingStyle, EasingDirection and the gamepad/touch/scroll input flags are accepted: a preview shows the settled layout, so page changes are instant. See UIPageLayout.
uiflexitemFlexMode (Grow / Fill / Custom), GrowRatio
uipaddingPaddingTop, PaddingBottom, PaddingLeft, PaddingRightA scale inset resolves against the node’s own size on a real axis, and against zero on an AutomaticSize axis — where it would be circular. Text measurement asks the same question as of 0.8.1.
uisizeconstraintMinSize, MaxSize
uiaspectratioconstraintAspectRatio, DominantAxisAspectTypeScaleWithParentSize is deferred.
uicornerCornerRadius, TopLeftRadius, TopRightRadius, BottomLeftRadius, BottomRightRadiusEach per-corner radius overrides CornerRadius for its own corner — that is how a card rounds only its top while its footer rounds only its bottom. The UIStroke ring and the UIShadow are box-shadows, so they follow the radius for free.
uistrokeColor, Thickness, Transparency, Enabled, BorderStrokePositionOuter (the default) spreads outward, Inner insets so the stroke eats into the object instead of inflating it, Center straddles the edge. Enabled = false or a fully transparent stroke paints nothing. ApplyStrokeMode — the ring is always drawn border-style, so Contextual on a text class does not outline glyphs.
uigradientColor, Rotation, EnabledOffset is ignored. The gradient overlays the background rather than multiplying it, and Transparency (a NumberSequence) is not typed at all.
uishadowColor, Offset, BlurRadius, Spread, Transparency, Enabled, ZIndexDrawn as a CSS drop shadow under the UIStroke ring.
uiscaleScale is ignored entirely.

UIListLayout and UIGridLayout report AbsoluteContentSize back after layout, gated on real change — so a control that sizes itself from Change={{ AbsoluteContentSize }} (a dropdown, say) gets a real number instead of collapsing to zero height.

SortOrder defaults to Name

Every UIGridStyleLayout defaults SortOrder to Enum.SortOrder.Name, which is the engine’s own default — so a list whose children carry distinct Names flows alphabetically, not in source order. Children with equal names keep source order (the sort is stable), which is why a tree that never sets Name is unaffected either way.

Loom defaulted to LayoutOrder until 0.7.0. If a list reordered itself when you upgraded, it was already ordering that way in Studio; set SortOrder={Enum.SortOrder.LayoutOrder} to get source / LayoutOrder order back.

UITableLayout

The layout’s siblings are the table’s lines — rows, or columns under MajorAxis.ColumnMajor — and each line’s own children are the cells:

<frame Size={UDim2.new(1, 0, 0, 120)}>
<uitablelayout Padding={UDim2.new(0, 8, 0, 4)} FillEmptySpaceColumns={true} />
<frame Name="Row1">
<textlabel Text="Region" Size={UDim2.fromOffset(120, 26)} />
<textlabel Text="Players" Size={UDim2.fromOffset(100, 26)} />
</frame>
</frame>

A column is as wide as its widest cell and a row as tall as its tallest, both measured against the table’s content box — so a 0.25 scale cell is a quarter of the table, not of its row. A line spans the whole table on its minor axis, which is what the engine reports for the row frames themselves; the line’s own Size is ignored, and so is any UIPadding on it (insetting each line separately would slide its cells off the columns they define).

FillEmptySpaceColumns / FillEmptySpaceRows scale the tracks proportionally so the table spans its container — in both directions, so a table whose natural width overruns is squeezed by the same rule. A hidden cell takes neither a track nor a gap.

UIPageLayout

Pages keep their own Size and sit one container-plus-Padding apart along FillDirection, so a parent with ClipsDescendants shows exactly one. Which page that is, is state — changed by method call as in Roblox, through a ref:

const pager = useRef<{ Next(): void; CurrentPageIndex: number }>();
<frame Size={UDim2.new(1, 0, 0, 90)} ClipsDescendants={true}>
<uipagelayout
ref={(instance) => { pager.current = instance as never; }}
Padding={UDim.new(0, 16)}
Circular={true}
/>
{/* …pages… */}
</frame>;

JumpToIndex(i) / JumpTo(page) / Next() / Previous() all work and fire PageLeavePageEnterStopped; Circular decides whether Next and Previous wrap at the ends.

Instance API

Every mounted object is a real instance with Roblox’s reflection surface on it, not a React element with a Roblox-shaped name. So a ref behaves the way a ref does in a place.

GroupWhat is there
IdentityClassName, Name, Parent, IsA, GetFullName
TreeGetChildren, GetDescendants, FindFirstChild (with recursive), FindFirstChildOfClass, FindFirstAncestor, FindFirstAncestorOfClass, FindFirstAncestorWhichIsA, IsDescendantOf
LifetimeDestroy, ClearAllChildren, and the Destroying / ChildAdded / ChildRemoved / AncestryChanged signals
Change notificationGetPropertyChangedSignal(name) and Changed, which fires with the property name
GeometryAbsolutePosition / AbsoluteSize, written back after each layout pass and signalled only when they actually move
AttributesGetAttribute, SetAttribute, GetAttributes, GetAttributeChangedSignal, AttributeChanged(0.11.0)
TextBoxCaptureFocus, ReleaseFocus, IsFocused
BindableEventEvent and Fire, class-scoped so no other class answers for those names

WaitForChild does not yield. The runtime is synchronous, so it returns the child if it is already there and otherwise warns and returns undefined rather than blocking the frame. Code that waits on a child built later in the same render needs restructuring, not patience.

Attributes

The second namespace on every instance, and the one an app owns outright, landed in 0.11.0. It behaves as it does in a place: an attribute is not readable as a property, does not fire Changed, and reaches neither the renderer nor the Scene IR — so writing one paints nothing and schedules no flush.

const player = Players.LocalPlayer;
player.GetAttributeChangedSignal("ColorScheme").Connect(() => {
print(player.GetAttribute("ColorScheme"));
});
player.SetAttribute("ColorScheme", "dark"); // fires
player.SetAttribute("ColorScheme", "dark"); // unchanged — silent, as in Roblox
player.SetAttribute("ColorScheme", undefined); // removes it, and still fires

GetAttributes() hands back a snapshot rather than the live store, and names are validated the way the engine validates them — up to 100 alphanumerics and underscores, with the RBX prefix reserved. A name a real place would refuse throws here rather than working in the preview and failing in Studio.

Input

Pointer and keyboard input route through the real runtime, not DOM shims: Activated, MouseButton1Click, InputBegan / InputChanged / InputEnded, UserInputService, focus and TextBox editing all behave as in Roblox, including input-sinking. Only TextButton, ImageButton, TextBox, ScrollingFrame and anything with Active = true sink pointer input — a transparent Frame is click-through, exactly as in Roblox.

Active governs sinking, not hearing: a plain Frame with an InputBegan handler — a slider handle, say — is hit-testable whether or not it is Active, while a frame with no listeners stays click-through so a transparent positioning layer does not swallow clicks.

What this list is not

It is a description of loom 0.11.0, not a compatibility promise. The project is pre-1.0 and rows move — an Accepted here is a gap someone can close without a breaking change. When a preview and Studio disagree and the property is not on this page, treat Studio as correct and file it.