Skip to main content

UI

A small declarative builder for instance trees.

The kit does not ship a dialog, but it does ship the thing you need to write one. This is that thing: enough structure to describe a tree in one expression, with none of the reactivity a real framework brings, so it never competes with the Fusion, React or Vide your game may already use.

const ui = UI.scoped(context.Janitor)

const card: Frame = ui("Frame", {
	Name = "TutorialCard",
	Size = UDim2.fromOffset(420, 160),
	BackgroundColor3 = Color3.fromRGB(18, 18, 22),

	[UI.Children] = {
		ui("UICorner", { CornerRadius = UDim.new(0, 10) }),
		ui("TextButton", {
			Text = "Continue",
			[UI.Event("Activated")] = function()
				print("clicked")
			end,
		}),
	},

	Parent = screenGui,
})

Three things a bare Instance.new does not do

Everything it creates, instances and connections alike, goes on the Janitor you scoped it to. A renderer written this way cannot outlive its step.

Parent is applied last, after properties and children. Parenting first makes the engine recompute layout on every following assignment; Fusion skips Parent in its property loop for the same reason.

A property the class does not have raises an error naming the class and the property, at construction, instead of failing silently.

On the return type

ui(...) returns any, so annotate the variable with the class you asked for and everything downstream of it is typed. Typing the property table per class needs generated types, which is why Fusion's New also gives up and returns a plain Instance. The property check above is what catches mistakes instead.

Types

KeyKind

type KeyKind = "Children" | "Event" | "Changed"

Key

interface Key {
KindKeyKind--

What the key does

Namestring--

Event or property name

}

A special key in a property table, produced by UI.Children, UI.Event or UI.Changed.

Props

type Props = {[string | Key]unknown}

A property table: class properties by name, plus the special keys.

Create

type Create = (
classNamestring,
propsProps?
) → any

What UI.scoped returns. Annotate the result with the class you asked for.

Properties

Children

This item is read only and cannot be modified. Read Only
UI.Children: Key

Marks the list of children in a property table.

ui("Frame", {
	[UI.Children] = {
		ui("UIListLayout", { Padding = UDim.new(0, 6) }),
		ui("TextLabel", { Text = "Hello" }),
	},
})

Children are parented before the frame itself is, which is the ordering that avoids a layout pass per property.

Functions

Event

UI.Event(
eventNamestring--

Name of the event on the class

) → Key--

Use it as a key in a property table

Connects a handler to an event.

ui("TextButton", {
	Text = "Continue",
	[UI.Event("Activated")] = function()
		request.Choices[1].Activate()
	end,
})

The connection goes on the scoped Janitor, so you never disconnect it by hand. An event the class does not have raises an error at construction.

Changed

UI.Changed(
propertyNamestring--

Property to watch

) → Key--

Use it as a key in a property table

Connects a handler to GetPropertyChangedSignal.

ui("Frame", {
	[UI.Changed("AbsoluteSize")] = function()
		relayoutCaption()
	end,
})

scoped

constructor
UI.scoped(
janitorJanitor--

Owns everything created through the returned function

) → Create--

Call it as ui("ClassName", props)

Binds a Janitor and returns the creator.

Inside an action, the Janitor to use is the step's:

Custom.new(function(context)
	const ui = UI.scoped(context.Janitor)

	const caption: TextLabel = ui("TextLabel", {
		Text = "Press the glowing button",
		Size = UDim2.fromOffset(320, 40),
		BackgroundTransparency = 1,
		Parent = context.Spotlight:GetOverlay(),
	})
end)

When the step ends, the caption and every connection made through ui go with it. In a dialog renderer, make your own Janitor and destroy it from DialogHandle.

Show raw api
{
    "functions": [
        {
            "name": "Event",
            "desc": "Connects a handler to an event.\n\n```lua\nui(\"TextButton\", {\n\tText = \"Continue\",\n\t[UI.Event(\"Activated\")] = function()\n\t\trequest.Choices[1].Activate()\n\tend,\n})\n```\n\nThe connection goes on the scoped Janitor, so you never disconnect it by hand. An event the\nclass does not have raises an error at construction.",
            "params": [
                {
                    "name": "eventName",
                    "desc": "Name of the event on the class",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "Use it as a key in a property table",
                    "lua_type": "Key"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 141,
                "path": "src/UI.luau"
            }
        },
        {
            "name": "Changed",
            "desc": "Connects a handler to `GetPropertyChangedSignal`.\n\n```lua\nui(\"Frame\", {\n\t[UI.Changed(\"AbsoluteSize\")] = function()\n\t\trelayoutCaption()\n\tend,\n})\n```",
            "params": [
                {
                    "name": "propertyName",
                    "desc": "Property to watch",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "Use it as a key in a property table",
                    "lua_type": "Key"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 166,
                "path": "src/UI.luau"
            }
        },
        {
            "name": "scoped",
            "desc": "Binds a Janitor and returns the creator.\n\nInside an action, the Janitor to use is the step's:\n\n```lua\nCustom.new(function(context)\n\tconst ui = UI.scoped(context.Janitor)\n\n\tconst caption: TextLabel = ui(\"TextLabel\", {\n\t\tText = \"Press the glowing button\",\n\t\tSize = UDim2.fromOffset(320, 40),\n\t\tBackgroundTransparency = 1,\n\t\tParent = context.Spotlight:GetOverlay(),\n\t})\nend)\n```\n\nWhen the step ends, the caption and every connection made through `ui` go with it. In a dialog\nrenderer, make your own Janitor and destroy it from [DialogHandle].",
            "params": [
                {
                    "name": "janitor",
                    "desc": "Owns everything created through the returned function",
                    "lua_type": "Janitor"
                }
            ],
            "returns": [
                {
                    "desc": "Call it as ui(\"ClassName\", props)",
                    "lua_type": "Create"
                }
            ],
            "function_type": "static",
            "tags": [
                "constructor"
            ],
            "source": {
                "line": 291,
                "path": "src/UI.luau"
            }
        }
    ],
    "properties": [
        {
            "name": "Children",
            "desc": "Marks the list of children in a property table.\n\n```lua\nui(\"Frame\", {\n\t[UI.Children] = {\n\t\tui(\"UIListLayout\", { Padding = UDim.new(0, 6) }),\n\t\tui(\"TextLabel\", { Text = \"Hello\" }),\n\t},\n})\n```\n\nChildren are parented before the frame itself is, which is the ordering that avoids a layout\npass per property.",
            "lua_type": "Key",
            "readonly": true,
            "source": {
                "line": 111,
                "path": "src/UI.luau"
            }
        }
    ],
    "types": [
        {
            "name": "KeyKind",
            "desc": "",
            "lua_type": "\"Children\" | \"Event\" | \"Changed\"",
            "source": {
                "line": 61,
                "path": "src/UI.luau"
            }
        },
        {
            "name": "Key",
            "desc": "A special key in a property table, produced by [UI.Children], [UI.Event] or [UI.Changed].",
            "fields": [
                {
                    "name": "Kind",
                    "lua_type": "KeyKind",
                    "desc": "What the key does"
                },
                {
                    "name": "Name",
                    "lua_type": "string",
                    "desc": "Event or property name"
                }
            ],
            "source": {
                "line": 71,
                "path": "src/UI.luau"
            }
        },
        {
            "name": "Props",
            "desc": "A property table: class properties by name, plus the special keys.",
            "lua_type": "{ [string | Key]: unknown }",
            "source": {
                "line": 82,
                "path": "src/UI.luau"
            }
        },
        {
            "name": "Create",
            "desc": "What [UI.scoped] returns. Annotate the result with the class you asked for.",
            "lua_type": "(className: string, props: Props?) -> any",
            "source": {
                "line": 90,
                "path": "src/UI.luau"
            }
        }
    ],
    "name": "UI",
    "desc": "A small declarative builder for instance trees.\n\nThe kit does not ship a dialog, but it does ship the thing you need to write one. This is that\nthing: enough structure to describe a tree in one expression, with none of the reactivity a real\nframework brings, so it never competes with the Fusion, React or Vide your game may already use.\n\n```lua\nconst ui = UI.scoped(context.Janitor)\n\nconst card: Frame = ui(\"Frame\", {\n\tName = \"TutorialCard\",\n\tSize = UDim2.fromOffset(420, 160),\n\tBackgroundColor3 = Color3.fromRGB(18, 18, 22),\n\n\t[UI.Children] = {\n\t\tui(\"UICorner\", { CornerRadius = UDim.new(0, 10) }),\n\t\tui(\"TextButton\", {\n\t\t\tText = \"Continue\",\n\t\t\t[UI.Event(\"Activated\")] = function()\n\t\t\t\tprint(\"clicked\")\n\t\t\tend,\n\t\t}),\n\t},\n\n\tParent = screenGui,\n})\n```\n\n### Three things a bare Instance.new does not do\n\nEverything it creates, instances and connections alike, goes on the Janitor you scoped it to. A\nrenderer written this way cannot outlive its step.\n\n`Parent` is applied last, after properties and children. Parenting first makes the engine\nrecompute layout on every following assignment; Fusion skips `Parent` in its property loop for\nthe same reason.\n\nA property the class does not have raises an error naming the class and the property, at\nconstruction, instead of failing silently.\n\n### On the return type\n\n`ui(...)` returns `any`, so annotate the variable with the class you asked for and everything\ndownstream of it is typed. Typing the property table per class needs generated types, which is\nwhy Fusion's `New` also gives up and returns a plain `Instance`. The property check above is\nwhat catches mistakes instead.",
    "source": {
        "line": 52,
        "path": "src/UI.luau"
    }
}