← Beginner tutorial: build an accessible counter

3. Replace src/main.luau

luastra/ui · app.render · app.handle · about 15 minutes
complete runnable application

Module state owns the count, render describes the current interface, and handle admits exactly two actions from stable controls.

When to use it

Replace the complete generated entry module; do not paste only render or handle because the imports, state, snapshot, and returned App.compose instance belong to the same file.

  • --!strict lets luastra check analyze this entire module.
  • app.render has no side effects: it returns a fresh description of UI from count.
  • onTap contains an action name; the clicked component's stable id arrives separately as target.
  • role=status announces the changed count without moving keyboard focus.
  • snapshot is a small test seam used by tests/smoke.luau; the host never calls it.

Focused example

--!strict

local UI = require("luastra/ui")

local App = require("luastra/app")
local Root = {}
local count: number = 0

function Root.render(): UI.Node
    return UI.Screen {
        id = "counter",
        documentTitle = "Beginner counter",
        width = "full",

        UI.Column {
            id = "counter/content",
            width = "content",
            padding = "responsive",
            gap = "md",

            UI.Text {
                id = "counter/title",
                text = "My first Luastra app",
                variant = "title",
            },
            UI.Text {
                id = "counter/value",
                text = `Count: {count}`,
                role = "status",
                label = `Current count: {count}`,
            },
            UI.Row {
                id = "counter/actions",
                gap = "sm",
                responsive = true,

                UI.Button {
                    id = "counter/add",
                    text = "Add one",
                    onTap = "counter.add",
                },
                UI.Button {
                    id = "counter/reset",
                    text = "Reset",
                    appearance = "secondary",
                    disabled = count == 0,
                    onTap = "counter.reset",
                },
            },
        },
    }
end

function Root.handle(action: string, target: string, _value: string)
    if action == "counter.add" and target == "counter/add" then
        count += 1
    elseif action == "counter.reset" and target == "counter/reset" then
        count = 0
    end
end

function Root.snapshot()
    return { count = count }
end

return App.compose {
    render = function(_outputs) return Root.render() end,
    handle = Root.handle,
    snapshot = Root.snapshot,
}

Complete inventory

View all pages in Beginner tutorial: build an accessible counter
← Previous: 2. Replace luastra.jsonNext: 4. Replace tests/smoke.luau →