Skip to content

Tree

local Tree = require("@game/ReplicatedStorage/Twill").Tree
local root, refs = Tree.Build({
ClassName = "Frame",
Name = "HUD",
Children = {
{ ClassName = "TextLabel", Ref = "Title", Text = "Hello" },
},
}, playerGui)
refs.Title.Text = "Ready"

A spec is one table per instance. Keys hold properties, so a spec reads like the Instance it becomes.

Six keys are reserved for structure and are never written as properties.

export type Spec = {
ClassName: string,
Name: string?,
Ref: string?,
Attributes: { [string]: any }?,
Events: { [string]: (...any) -> () }?,
Children: { Spec }?,
[string]: any,
}
KeyMeaning
ClassNameThe kind of instance to create. The only required key.
NameWhat the instance is called.
ChildrenA list of further specs, built below this one.
AttributesWritten with SetAttribute, after the properties.
EventsSignal names mapped to callbacks. Connected after the properties.
RefA name this instance is handed back under.

Any key not in that table is written to the instance directly, so an unknown property name fails where it is written.

Properties are written first, then attributes, then events are connected, and children are built last.

That order buys one guarantee: an initial property write never fires your own handler, because nothing is connected while those writes happen. A GetPropertyChangedSignal handler passed through Events hears only the changes made afterwards.

Events are connected before the children exist. That suits interaction handlers, which run long after the build. A handler that could fire immediately cannot assume the rest of the tree is there.

[Server] | [Client]

Builds an instance tree from a spec and hands back the pieces worth keeping.

function Tree.Build(spec: Spec, parent: Instance?, owner: Bag?): (Instance, { [string]: Instance })

Parameters

NameTypeDescription
specSpecDescribes the root instance and everything under it.
parentInstance?Where the root goes. Left unparented when omitted.
ownerBag?A bag that destroys the tree, and its events with it, when it closes.

Returns

Instance - The root of the finished tree.

{ [string]: Instance } - Every instance that asked for a Ref, by that name, however deep in the tree it sat.

Throws when a spec anywhere in the tree has no ClassName string, and when owner is not a bag.

Children are parented to their own parent as they are built, and the root is parented last. A tree that fails partway leaves nothing behind in the world, because the root never reached the parent given.

Parenting is the caller’s, so one spec may be built into several places at once.

Example

local root, refs = Tree.Build({
ClassName = "Frame",
Name = "CoinCounter",
Size = UDim2.fromOffset(180, 40),
BackgroundTransparency = 0.3,
Children = {
{
ClassName = "TextLabel",
Ref = "Amount",
Size = UDim2.fromScale(1, 1),
Text = "0",
},
{
ClassName = "TextButton",
Text = "Collect",
Events = {
Activated = onCollect,
},
},
},
}, playerGui)
Twill.Replication.Subscribe("Data.Coins", function(coins)
refs.Amount.Text = Twill.Format.Comma(coins or 0)
end)

Refs are collected from the whole tree, so a nested label is reached without a chain of FindFirstChild calls.

Events connections last until the tree is destroyed. That is enough for UI parented to a PlayerGui, since Roblox removes it with the player.

For anything longer lived, or anything parented into the world, pass a Scope bag as the owner. Closing the bag destroys the tree and disconnects its events:

local root = Tree.Build(spec, workspace, bag)

Tree builds instances once. It does not re-render them when state moves, and it holds no view state. Follow a value by subscribing to it and writing through a Ref, or bring a UI library when reconciliation is wanted.