Loomreference

Supported instances and properties

The per-property support matrix — rendered, accepted and ignored, or not typed at all.

Scope and status answers “is text done?” at the area level. This page answers “does AutomaticCanvasSize work?” at the property level.

Everything below describes loom 0.12.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. 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×.
uiflexitemSupportedFlexMode — Grow / 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. Font metrics do not exist inside the WASM engine. The adapter runs a canvas measureText() with the font the renderer paints, and hands the result over as TextBounds. Which typeface that is, and why it is worth pinning, is Fonts and text metrics.

TextWrapped is honoured, and runs are laid into lines at word boundaries. One function decides every wrap. Measurement asks it how many lines a label needs, and the text layer asks where the breaks go. The advances behind it are the engine’s.

The wrap width is the nearest ancestor with 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 never wraps.

The re-wrap settles inside the paint that caused it. The first measurement after a container narrows is against the old width. The adapter re-lays-out until the two agree, up to four passes before deferring to the next frame. It patches the DOM once.

A RichText string is measured run by run in the font its tags ask for, so a bold run does not clip. An empty TextBox is measured against its PlaceholderText, or an AutomaticSize.Y input would collapse to zero height.

Both adapters do this, so the same scene lays out identically through vide and react.

The wrap width also carries the UIPadding insets in between, including scale ones. A scale inset resolves against the node’s own width where its X axis is real, and against zero where the axis is automatic. Otherwise the width would set the padding, which would set the width.

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. CSS font-size sets the em square, and a face’s ascent plus descent runs well past 1em.

Loom divides by the face box rather than painting font-size: TextSize straight through, which would draw every glyph too big. LineHeight sets the line box in pixels off TextSize. The ratio is calibrated per family — see TextSize is not a font size.

The clip rect carries the overhang the browser’s taller line boxes need, and padding hands the content box its original height back. Otherwise a paragraph’s last line loses its descenders. Nothing about the layout moves, and a label still clips its text at its left and right edges.

AutomaticSize is bounded by the parent

Roblox bounds automatic growth: an AutomaticSize object grows up to the maximum its parent allows, and a TextWrapped label grows to that maximum and only then wraps.

Every auto-sized node carries the ceiling its parent leaves, inherited through the padding in between. Size remains the floor even past it, and a wrapping UIListLayout wraps against the same ceiling. Content that still does not fit overflows, as it does in the engine.

A ScrollingFrame is the exception. Its children get no ceiling on an axis carrying AutomaticCanvasSize or a CanvasSize of 0. A CanvasSize giving the axis a real extent is still a ceiling.

Image properties

Shared by imagelabel and imagebutton. The image paints in a layer beneath the text, as a background-painted element rather than an <img>. A sprite window and a 9-slice both place a region of the source.

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, DominantAxisAspectType — ScaleWithParentSize 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.
uiscale—Scale is ignored entirely.

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

SortOrder defaults to Name

Every UIGridStyleLayout defaults SortOrder to Name, the engine’s own default — so a list whose children carry distinct Names flows alphabetically, not in source order. The sort is stable, so a tree that never sets Name is unaffected.

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 order back.

UITableLayout

The layout’s siblings are the table’s lines: rows, or columns under MajorAxis.ColumnMajor. 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 are 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. Its own Size is ignored, as is any UIPadding on it, which would otherwise slide its cells off the columns they define.

FillEmptySpaceColumns and FillEmptySpaceRows scale the tracks proportionally so the table spans its container. It works 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() and Previous() all work and fire PageLeave → PageEnter → Stopped. 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, Clone with Archivable — (0.12.0) — 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 rather than blocking the frame. Code that waits on a child built later in the same render needs restructuring, not patience.

A property read before it is written answers with a typed default, the way Roblox reflection does rather than with undefined. That is 0 for a number, "" for a string, and the class’s own default for a datatype. A UI that reads frame.BackgroundTransparency before setting it used to get undefined and propagate a NaN through whatever it computed.

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. 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. 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, input-sinking included.

0.12.0 closed the gap where the rest of the mouse events were connectable but never dispatched, which is worse than absent. A handler attached to one silently did nothing. MouseButton1Down, MouseButton1Up, MouseButton2Click and MouseMoved fire now. Keyboard input reaches the runtime too, so UserInputService.IsKeyDown and a KeyCode comparison answer what the user actually pressed. Enum.KeyCode gained its digits, modifiers and gamepad buttons in the same release, which is what a keyboard-driven UI was missing to match against. 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, such as a slider handle, is hit-testable whether or not it is Active. 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.12.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.