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
| Mark | Meaning |
|---|---|
| Supported | Read by the layout engine or painted by the renderer. Behaves like Roblox, within the fidelity limits below. |
| Partial | Implemented, with a stated gap. Correct in the common case, wrong at the edge. |
| Accepted | Typed and settable, and nothing reads it. No error, no effect. |
| Not typed | No 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.
The engine takes the string wherever it takes the item — AutomaticSize="XY" is
Enum.AutomaticSize.XY, and roblox-ts’s own React typings offer both spellings. Loom reads either,
everywhere, as of 0.9.1. Before that release the Scene IR encoded a string happily but every place
the adapters read one back off an instance insisted on an EnumItem: a label written
AutomaticSize="XY" was auto-sized by the layout and measured by nobody, so it collapsed to zero and
spilled its text. FontSize written as a string was ignored the same way.
Instances
| Intrinsic | Status | Notes |
|---|---|---|
screengui surfacegui billboardgui | Supported | LayerCollectors: never background-painted, always click-through, z-ordered among themselves by DisplayOrder. |
frame | Supported | |
scrollingframe | Supported | Sinks pointer input unconditionally, like Roblox. |
canvasgroup | Supported | GroupTransparency becomes CSS opacity on the container div, which is the same “composite the subtree, then fade it” model. |
textlabel textbutton textbox | Supported | See Text. |
imagelabel imagebutton | Supported | The Image paints in a layer beneath the text, in every ScaleType. See Image properties. |
viewportframe videoframe | Partial | Typed and laid out as plain GuiObjects. No 3D scene, no video — they paint as backgrounds. |
uilistlayout uigridlayout uipadding | Supported | |
uipagelayout uitablelayout | Supported | Implemented in 0.7.0. See Layout and modifier properties. |
uicorner uistroke uigradient | Supported | Mapped to border-radius (per-corner), a box-shadow ring on the side BorderStrokePosition asks for, and a CSS linear-gradient. |
uishadow | Supported | A CSS drop shadow, layered under a UIStroke ring rather than replacing it. |
uisizeconstraint uiaspectratioconstraint | Supported | |
uiscale | Accepted | Typed, recognized as a modifier, and read by nothing. A scaled subtree renders at 1×. |
uiflexitem | Supported | FlexMode — Grow / Fill, or Custom with GrowRatio. |
| Everything else | Not typed | uitextsizeconstraint, folder, part, … — the intrinsic set is exactly what the renderer implements. |
GuiObject properties
Shared by every visual class.
| Property | Status | Notes |
|---|---|---|
Size Position AnchorPoint | Supported | Full UDim2 scale + offset resolution. |
AutomaticSize | Supported | X / 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 BackgroundTransparency | Supported | |
Visible | Supported | Hidden via CSS; the node still occupies its computed rect, as in Roblox. |
ZIndex | Supported | ZIndexBehavior.Sibling semantics — the renderer’s native model. |
LayoutOrder | Supported | Honored by every layout under SortOrder.LayoutOrder — which is not the default. See SortOrder defaults to Name. |
Rotation | Supported | Degrees clockwise about the element center, as a CSS transform. Purely visual: the layout rect is unrotated, matching Roblox. |
ClipsDescendants | Supported | |
Active | Partial | Honored 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. |
Name | Supported | |
| (unwritten properties) | Supported | Reading a property nobody has written yields the Roblox default — Visible, ZIndex, BackgroundTransparency, Rotation, LayoutOrder, Active, ClipsDescendants, AnchorPoint, Position, Size — not undefined. |
Event Change ref | Supported | Event={{ Activated }}, Change={{ Text }}, and ref to the live LoomInstance. |
BorderSizePixel BorderColor3 SizeConstraint Transparency Interactable | Not typed |
ScreenGui properties
| Property | Status | Notes |
|---|---|---|
DisplayOrder | Supported | Becomes the layer’s CSS z-index (may be negative). |
Enabled | Supported | |
ZIndexBehavior | Accepted | Sibling is what the renderer already does; Global is not emulated. |
IgnoreGuiInset | Accepted | The runtime’s GetGuiInset() is zero, so there is no inset to ignore. |
ResetOnSpawn ScreenInsets | Accepted | Typed so real code runs unchanged; nothing reads them. |
Text properties
| Property | Status | Notes |
|---|---|---|
Text TextColor3 TextSize TextTransparency | Supported | A 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 TextYAlignment | Supported | |
TextWrapped | Supported | white-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. |
LineHeight | Supported | The 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. |
FontFace | Supported | new 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. |
Font | Supported | The 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. |
FontSize | Supported | The 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. |
RichText | Supported | <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. |
TextScaled | Accepted | Never applied. Text renders at TextSize whether or not it fits its box. |
TextTruncate MaxVisibleGraphemes TextStrokeColor3 TextStrokeTransparency | Not typed | No 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.
| Property | Status | Notes |
|---|---|---|
Image | Supported | http(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. |
ImageTransparency | Supported | |
ImageColor3 | Supported | A 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. |
ScaleType | Supported | All five as of 0.7.0: Stretch, Fit, Crop, Slice and Tile. |
SliceCenter SliceScale | Supported | The 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. |
TileSize | Supported | One 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 ImageRectSize | Supported | The 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. |
ResampleMode | Supported | Pixelated 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.Tilewith anImageRectwindow. CSS backgrounds cannot repeat a region, so the whole image tiles and loom warns once, naming the node.- A
SliceCentermeasured against a window rather than against the whole image. The slice insets are read from the source’s natural size.
Roblox’s thumbnail API sends no CORS headers, so the browser cannot resolve an asset id on its own.
@loom-dev/preview installs a resolver for both cases:
- Under a dev server (
loom preview, the embedded server, Next dev) the id is resolved server-side and answered as a redirect from<base>__loom/asset/<id>. - In a static build (
loom build,vite build) there is no server later, so the ids are resolved at build time, the images are downloaded into the output, and a__loom/assets.jsonmanifest points the page at them — see Static builds and embedding.
The build finds those ids two ways: it reads the emitted output for rbxassetid://<digits>, and —
since 0.9.5 — it mounts each gallery target in node and reads the live tree, which is what catches
an id composed at runtime (`rbxassetid://${iconId}`). An image the first render never reaches
is still missed. Anywhere else, install your own resolver with setImageResolver from
@loom-dev/renderer.
TextBox properties
| Property | Status | Notes |
|---|---|---|
PlaceholderText | Supported | Becomes the DOM input’s placeholder. |
ClearTextOnFocus TextEditable MultiLine | Supported | |
PlaceholderColor3 | Accepted | The placeholder is not restyled — you get the browser default. |
ScrollingFrame properties
| Property | Status | Notes |
|---|---|---|
CanvasSize | Supported | Children lay out against the canvas, not the window. |
CanvasPosition | Supported | Applied as the DOM scroll offset by the renderer, not by the layout engine. |
AutomaticCanvasSize | Supported | X / 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 ScrollingEnabled | Supported | Decide which axes can scroll, and therefore which bars can appear. |
ScrollBarThickness | Supported | The bar’s width, default 12. A thickness of 0 shows no bar, as in Roblox. |
ScrollBarImageColor3 | Supported | Tints 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. |
ScrollBarImageTransparency | Accepted | The thumb is drawn opaque. |
VerticalScrollBarInset HorizontalScrollBarInset | Accepted | The 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. |
A scrolling list in Roblox is an AutomaticSize column inside an AutomaticCanvasSize frame, and
loom capped every child’s automatic growth at its parent’s box — right for a 45% column, wrong for
a canvas, where outgrowing the window is the whole point. The column came out exactly the window’s
height, so the canvas equalled the window, nothing overflowed and nothing scrolled. And with no bar
drawn, a frame that did have something to scroll looked like a static clipped box.
A ScrollingFrame now leaves its children no ceiling on an axis whose canvas is free to grow —
AutomaticCanvasSize, or a CanvasSize of 0 on that axis. A CanvasSize that gives the axis a real
extent is still the ceiling it always was. The bar is drawn from the same window/canvas math the
adapters feed back as AbsoluteWindowSize/AbsoluteCanvasSize, so it cannot disagree with the
numbers a component sized itself against.
AutomaticSize on a ScrollingFrame that also sets CanvasSize is explicitly deferred in the
layout engine. The combination will not error; it will not size the way Studio does either.
Layout and modifier properties
| Instance | Supported | Accepted / partial |
|---|---|---|
uilistlayout | FillDirection, HorizontalAlignment, VerticalAlignment, SortOrder, Padding, HorizontalFlex, VerticalFlex, Wraps | — |
uigridlayout | CellSize, CellPadding, FillDirection, FillDirectionMaxCells, HorizontalAlignment, VerticalAlignment, SortOrder | StartCorner — only TopLeft is implemented; the other three corners fall back to it. |
uitablelayout | MajorAxis, Padding, FillEmptySpaceColumns, FillEmptySpaceRows, HorizontalAlignment, VerticalAlignment, SortOrder | See UITableLayout. |
uipagelayout | FillDirection, Padding, Circular, HorizontalAlignment, VerticalAlignment, SortOrder | Animated, 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. |
uiflexitem | FlexMode (Grow / Fill / Custom), GrowRatio | — |
uipadding | PaddingTop, PaddingBottom, PaddingLeft, PaddingRight | A 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. |
uisizeconstraint | MinSize, MaxSize | — |
uiaspectratioconstraint | AspectRatio, DominantAxis | AspectType — ScaleWithParentSize is deferred. |
uicorner | CornerRadius, TopLeftRadius, TopRightRadius, BottomLeftRadius, BottomRightRadius | Each 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. |
uistroke | Color, Thickness, Transparency, Enabled, BorderStrokePosition | Outer (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. |
uigradient | Color, Rotation, Enabled | Offset is ignored. The gradient overlays the background rather than multiplying it, and Transparency (a NumberSequence) is not typed at all. |
uishadow | Color, Offset, BlurRadius, Spread, Transparency, Enabled, ZIndex | Drawn 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 — 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 PageLeave →
PageEnter → Stopped; Circular decides whether Next and Previous wrap at the ends.
Roblox’s CurrentPage is a GuiObject reference, and a Scene IR property value is a datatype —
never a node — so it cannot cross the wasm boundary. The layout engine reads CurrentPageIndex, a
0-based int the runtime keeps in step; CurrentPage still reads back as the instance for your code.
That only matters if you drive a pager from something other than the @rbxts/react adapter: set
CurrentPageIndex on the node yourself.
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.
| Group | What is there |
|---|---|
| Identity | ClassName, Name, Parent, IsA, GetFullName |
| Tree | GetChildren, GetDescendants, FindFirstChild (with recursive), FindFirstChildOfClass, FindFirstAncestor, FindFirstAncestorOfClass, FindFirstAncestorWhichIsA, IsDescendantOf |
| Lifetime | Destroy, ClearAllChildren, and the Destroying / ChildAdded / ChildRemoved / AncestryChanged signals |
| Change notification | GetPropertyChangedSignal(name) and Changed, which fires with the property name |
| Geometry | AbsolutePosition / AbsoluteSize, written back after each layout pass and signalled only when they actually move |
| Attributes | GetAttribute, SetAttribute, GetAttributes, GetAttributeChangedSignal, AttributeChanged — (0.11.0) |
TextBox | CaptureFocus, ReleaseFocus, IsFocused |
BindableEvent | Event 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"); // firesplayer.SetAttribute("ColorScheme", "dark"); // unchanged — silent, as in Robloxplayer.SetAttribute("ColorScheme", undefined); // removes it, and still firesGetAttributes() 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.