Skip to content

Schema reference ​

A window is a Lua table. Everything in it must be plain data (no functions): it is sent to the page as one JSON message.

lua
tm.ui.window({
    id = 'settings', title = 'Settings', icon = 'lucide:settings',
    values = { audio = { volume = 60 } },
    children = { ... },
    footer = { ... },
})

Window ​

FieldTypeDefault
idstringgeneratedRe-using an id replaces that window.
title, subtitlestringHeader text. No title, no header.
eyebrowstringSmall uppercase label above the title.
iconstringIconify id (lucide:car), item:<name> for an inventory item image, or an image URL.
tonestringblueHeader icon tone: neutral blue green red amber violet teal.
headerNodesnode[]Nodes in the header, right of the title: step dots, badges, small buttons.
widthnumber or 'auto'440Pixels (capped to the screen), or 'auto' to size to the content (hint bars, toasts).
positionstringcentercenter left right top bottom top-left top-right bottom-left bottom-right.
x, ynumberExact pixel position from the top-left corner. Overrides position.
draggablebooleantrueDrag the window by its header (kept on screen). Not for passive windows.
animationstringpoppop, slide-right, slide-left, fade, none. Applies when the window opens and closes.
closablebooleantrueShow the X and allow Escape.
passivebooleanfalseAmbient window: ignores the mouse, can't be dragged, never closed by Escape. Pair with focus = false.
focusbooleantrueTake NUI focus. false for display-only windows.
keysbooleanfalseForward keydown / keyup events while this window is on top.
styletableCustom styles for the window. Card look (background, border, borderRadius, boxShadow, backdropFilter...) plus frame placement (position, top/left/right/bottom, transform, margins, width/height). See Styling.
bodyStyletableStyles for the body area under the header (padding, gap, background...).
valuestableInitial field values. Nested tables are flattened to dotted paths.
childrennode[]Body.
footernode[]Right-aligned row under the body, usually buttons.

Common node fields ​

Field
typeRequired. One of the types below.
idHandle for win:setNode(id, props).
pathFlat dotted key for the value (user.name). Fields store their value here.
defaultInitial value when values doesn't provide one.
whenShow only while the condition holds: { path, eq }, { path, neq } or { path, truthy = true/false }.
disabledWhenSame condition shape; disables the node while it holds.
disabledStatic disable.
rulesValidation, see below.
notifytrue: send a debounced change event to Lua when this field changes.
styleA few visual tweaks, see Styling.

when and disabledWhen run in the page against current field values. They never talk to Lua.

Layout ​

TypeFields
sectiontitle, description, icon, collapsible, open (default true), flat, children.
rowchildren, gap (px), align (start center end; default start). Children share width equally; a child may set grow (flex factor) or width (px).
gridCSS grid. columns (default 2) or minWidth (px: as many columns as fit, each at least that wide), gap (default 12), children. A child may set span (columns to span).
stackVertical group. children, gap (default 12), padding, align = 'center' for centred text.
wrapInline group that wraps; children keep natural width (badges, small text).
tabstabs = { { id, label, icon, children } }. The active tab id is stored at path (optional).
divider, spacerspacer takes size (px).

Display ​

TypeFields
texttext, tone = muted title danger success, markdown (see below). Newlines are kept.
noticetext, icon, tone = neutral info warn danger.
meterlabel, value (0-100), icon, warnBelow (default 50), badBelow (default 25). Turns amber/red as it drops.
badgelabel, tone, icon, dot.
infolabel, value, icon, mono, tone = default muted good warn bad.
barlabel, value (0-100), text (right side; default the percentage).
statBig-number card. label, value, icon, tone (neutral blue green red amber violet teal), hint. Put several in a row.
icon, imageicon (Iconify id, item:<name>) or src (URL), size (px; default 20 icon / 64 image), tone, spin (rotate; for spinners), align = 'center', rounded = false.
avatarRound portrait + text. src or icon (else initials of name), name, subtitle, badge, badgeTone, size (default 44).
idcardDocument card: issuer band, portrait, labelled fields, number footer. title, authority (small line above the title), icon, tone (neutral blue green red amber violet teal), status (valid expired suspended, or any text) with statusLabel, photo (URL; a placeholder shows when missing or broken), name, `fields = { { label, value, tone = 'muted'
dotsStep indicator. total, current (1-based), done (marks the current step complete). Usually in headerNodes.
chartInline SVG chart, no dependencies. kind = bar line spark, data (numbers, or { label, value }), tone, height (px), min/max, labels = true (bar labels), label (title).
kbdKey hint. keys (string or list), label.
copyValue with a copy button. label, value.
timerCountdown that runs in the page (no messages while it ticks). duration (ms), label, showTime, event (sent once at zero; default timeout), close. Change duration with setNode to restart it.

Collections ​

TypeFields
listSelectable rows. items = { { key, label, description, icon, caption, badge, badgeTone, disabled } }, search (auto above 8 items), pageSize (default 8), empty, path (stores the selected key), event (sent as { key } when a row is clicked).
tablecolumns = { { key, label, width, align } }, rows = { { key, <column key> = value, ... } }, search (auto above 10 rows), pageSize (default 10), empty, path, event (row click, sent as { key }).
tilesGrid of large action buttons. columns (default 3), items = { { label, icon, hint, tone, event, args, close, set, toggle, disabled } }.
menuNavigable list with submenus and checkboxes: see menus.

Lists and tables search, filter and page inside the page. Sending very large row sets is still a cost, so keep them to a few hundred rows.

Fields ​

All fields need a path, and accept label, hint, disabled, rules, notify.

TypeValueExtra fields
inputstringplaceholder, icon, prefix, suffix, clearable, submitOnEnter (an event name: Enter submits with that event).
numbernumber or nilas input, plus min, max, step.
textareastringplaceholder, rows.
selectoption valueoptions (strings, or { label, value, icon, description, disabled }), placeholder, clearable, searchable (auto above 8 options).
togglebooleandescription, boxed.
slidernumbermin, max, step, icon, suffix.
color#rrggbb stringalpha, clearable.
segmentedoption valueoptions (strings or { label, value, icon, disabled }), size, block (default true). Good for 2-4 choices.
chipslist of valuesoptions, searchable (auto above 12). Multi-select; the value is an array.
checkboxbooleanAlias of toggle.

Button ​

Field
label, iconText and icon.
variantprimary secondary ghost danger success warning subtle-danger.
sizexs sm md lg.
block, alignFull width; align = 'start' left-aligns the content.
eventName of the event sent to Lua (default click).
submittrue: validate every visible field first; if any fails, nothing is sent. The event then carries values.
closetrue: close the window after sending.
argsAny data; comes back untouched in the event as data.args.
disabled, disabledWhen
set{ path = 'step', value = 2 }: set a value in the page, no message to Lua.
toggleA path: flip that boolean in the page.

A button with set or toggle and no event, submit or close stays entirely in the page. Combined with when, that is enough for wizards and simple show/hide without any Lua:

lua
values = { step = 1 },
children = {
    { type = 'stack', when = { path = 'step', eq = 1 }, children = { ... } },
    { type = 'stack', when = { path = 'step', eq = 2 }, children = { ... } },
},
footer = {
    { type = 'button', label = 'Back', set = { path = 'step', value = 1 }, when = { path = 'step', eq = 2 } },
    { type = 'button', label = 'Next', set = { path = 'step', value = 2 }, when = { path = 'step', eq = 1 }, variant = 'primary' },
    { type = 'button', label = 'Done', event = 'done', submit = true, close = true, when = { path = 'step', eq = 2 } },
},

tiles items take the same event, args, close, set and toggle.

menu renders a navigable list. See menus. Most callers use tm.ui.menu rather than writing the node.

Validation rules ​

lua
rules = { required = true, min = 18, max = 90, minLength = 2, maxLength = 32, pattern = '^[0-9]{3,5}$', message = '3-5 digits' }
  • Errors show after the field is touched or after a submit attempt.
  • Hidden fields (when is false) are skipped.
  • pattern is a JavaScript regular expression source.
  • message replaces the generated text.
  • Rules are for UX. Validate again on the server for anything that matters.

Values ​

  • Flat dotted paths: values = { user = { name = 'x' } } is stored as user.name.
  • Arrays are values, not paths.
  • An empty Lua table becomes [] in JSON. The page treats an empty values as "no values"; use false (not nil) for a value you want to keep, because nil fields vanish from tables.
  • Submit events carry every value, including hidden fields and untouched defaults.

Markdown text ​

text nodes with markdown = true render a small, safe subset:

WriteGet
**bold**bold
*italic*italic
`code`code chip
[[G]]a key cap showing G

Everything is drawn as text and elements, never as HTML, so a string from a player or database can't inject markup.

lua
{ type = 'text', markdown = true, text = 'Hold [[G]] to open the **radial menu**. Type `/tutorial skip` to skip.' }

Styling ​

Any node, and the window itself (style, bodyStyle), can carry a style table. Only a fixed set of properties is applied, so a schema can restyle itself but can't load resources or break out of its box. Keys can be camelCase (borderRadius) or CSS-style ('border-radius').

lua
{ type = 'text', text = 'Notice me', style = { padding = 12, background = 'rgba(59,130,246,0.15)', borderRadius = 10, textAlign = 'center', color = '#93c5fd' } }

tm.ui.window({
    width = 'auto',
    style = {
        background = 'linear-gradient(135deg, rgba(15,23,42,0.9), rgba(30,41,59,0.8))',
        border = '1px solid rgba(255,255,255,0.12)', borderRadius = 20,
        boxShadow = '0 10px 40px rgba(0,0,0,0.5)', backdropFilter = 'blur(8px)',
        position = 'absolute', left = '50%', top = 24, transform = 'translateX(-50%)',   -- placement
    },
    bodyStyle = { padding = 8, gap = 6 },
    children = { ... },
})
  • Lengths (numbers become px; strings such as '50%', '2rem' or '60vmin' also work): width, minWidth, maxWidth, height, minHeight, maxHeight, padding*, margin*, gap, rowGap, columnGap, flexBasis, borderRadius (and the four corners), borderWidth (and sides), fontSize, letterSpacing.
  • Look: opacity, color, background, backgroundColor, backgroundImage (gradients only), backgroundSize, backgroundPosition, border (and borderTop/Right/Bottom/Left), borderColor, borderStyle, boxShadow, backdropFilter, filter.
  • Text: textAlign, fontWeight, fontStyle, lineHeight, textTransform, textDecoration, whiteSpace, textOverflow.
  • Layout inside the node: alignSelf, justifySelf, alignItems, alignContent, justifyItems, justifyContent, placeItems, flex, flexGrow, flexShrink, flexDirection, flexWrap, order, gridColumn, gridRow, gridTemplateColumns, gridTemplateRows, gridAutoFlow, display (block flex grid inline inline-flex none), overflow(X/Y), aspectRatio, objectFit.
  • Window only (style on the window, ignored on nodes): position (static relative absolute fixed), top, right, bottom, left, transform, transformOrigin. Sizes, margins and these move the frame; everything else paints the card.
  • Values are limited to letters, numbers, #, %, ., ,, (), /, +, -, * and spaces (200 characters). url(), image-set(), var(), attr(), env(), expression(), @import, javascript:, !important and anything with ; or braces is dropped.
  • Anything not on the list (zIndex, animation, pointerEvents...) is ignored.
  • A styled node is wrapped in one extra element, so style on a row child affects the wrapper.