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.
tm.ui.window({
id = 'settings', title = 'Settings', icon = 'lucide:settings',
values = { audio = { volume = 60 } },
children = { ... },
footer = { ... },
})Window
| Field | Type | Default | |
|---|---|---|---|
id | string | generated | Re-using an id replaces that window. |
title, subtitle | string | Header text. No title, no header. | |
eyebrow | string | Small uppercase label above the title. | |
icon | string | Iconify id (lucide:car), item:<name> for an inventory item image, or an image URL. | |
tone | string | blue | Header icon tone: neutral blue green red amber violet teal. |
headerNodes | node[] | Nodes in the header, right of the title: step dots, badges, small buttons. | |
width | number or 'auto' | 440 | Pixels (capped to the screen), or 'auto' to size to the content (hint bars, toasts). |
position | string | center | center left right top bottom top-left top-right bottom-left bottom-right. |
x, y | number | Exact pixel position from the top-left corner. Overrides position. | |
draggable | boolean | true | Drag the window by its header (kept on screen). Not for passive windows. |
animation | string | pop | pop, slide-right, slide-left, fade, none. Applies when the window opens and closes. |
closable | boolean | true | Show the X and allow Escape. |
passive | boolean | false | Ambient window: ignores the mouse, can't be dragged, never closed by Escape. Pair with focus = false. |
focus | boolean | true | Take NUI focus. false for display-only windows. |
keys | boolean | false | Forward keydown / keyup events while this window is on top. |
style | table | Custom 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. | |
bodyStyle | table | Styles for the body area under the header (padding, gap, background...). | |
values | table | Initial field values. Nested tables are flattened to dotted paths. | |
children | node[] | Body. | |
footer | node[] | Right-aligned row under the body, usually buttons. |
Common node fields
| Field | |
|---|---|
type | Required. One of the types below. |
id | Handle for win:setNode(id, props). |
path | Flat dotted key for the value (user.name). Fields store their value here. |
default | Initial value when values doesn't provide one. |
when | Show only while the condition holds: { path, eq }, { path, neq } or { path, truthy = true/false }. |
disabledWhen | Same condition shape; disables the node while it holds. |
disabled | Static disable. |
rules | Validation, see below. |
notify | true: send a debounced change event to Lua when this field changes. |
style | A few visual tweaks, see Styling. |
when and disabledWhen run in the page against current field values. They never talk to Lua.
Layout
| Type | Fields |
|---|---|
section | title, description, icon, collapsible, open (default true), flat, children. |
row | children, gap (px), align (start center end; default start). Children share width equally; a child may set grow (flex factor) or width (px). |
grid | CSS 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). |
stack | Vertical group. children, gap (default 12), padding, align = 'center' for centred text. |
wrap | Inline group that wraps; children keep natural width (badges, small text). |
tabs | tabs = { { id, label, icon, children } }. The active tab id is stored at path (optional). |
divider, spacer | spacer takes size (px). |
Display
| Type | Fields |
|---|---|
text | text, tone = muted title danger success, markdown (see below). Newlines are kept. |
notice | text, icon, tone = neutral info warn danger. |
meter | label, value (0-100), icon, warnBelow (default 50), badBelow (default 25). Turns amber/red as it drops. |
badge | label, tone, icon, dot. |
info | label, value, icon, mono, tone = default muted good warn bad. |
bar | label, value (0-100), text (right side; default the percentage). |
stat | Big-number card. label, value, icon, tone (neutral blue green red amber violet teal), hint. Put several in a row. |
icon, image | icon (Iconify id, item:<name>) or src (URL), size (px; default 20 icon / 64 image), tone, spin (rotate; for spinners), align = 'center', rounded = false. |
avatar | Round portrait + text. src or icon (else initials of name), name, subtitle, badge, badgeTone, size (default 44). |
idcard | Document 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' |
dots | Step indicator. total, current (1-based), done (marks the current step complete). Usually in headerNodes. |
chart | Inline SVG chart, no dependencies. kind = bar line spark, data (numbers, or { label, value }), tone, height (px), min/max, labels = true (bar labels), label (title). |
kbd | Key hint. keys (string or list), label. |
copy | Value with a copy button. label, value. |
timer | Countdown 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
| Type | Fields |
|---|---|
list | Selectable 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). |
table | columns = { { 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 }). |
tiles | Grid of large action buttons. columns (default 3), items = { { label, icon, hint, tone, event, args, close, set, toggle, disabled } }. |
menu | Navigable 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.
| Type | Value | Extra fields |
|---|---|---|
input | string | placeholder, icon, prefix, suffix, clearable, submitOnEnter (an event name: Enter submits with that event). |
number | number or nil | as input, plus min, max, step. |
textarea | string | placeholder, rows. |
select | option value | options (strings, or { label, value, icon, description, disabled }), placeholder, clearable, searchable (auto above 8 options). |
toggle | boolean | description, boxed. |
slider | number | min, max, step, icon, suffix. |
color | #rrggbb string | alpha, clearable. |
segmented | option value | options (strings or { label, value, icon, disabled }), size, block (default true). Good for 2-4 choices. |
chips | list of values | options, searchable (auto above 12). Multi-select; the value is an array. |
checkbox | boolean | Alias of toggle. |
Button
| Field | |
|---|---|
label, icon | Text and icon. |
variant | primary secondary ghost danger success warning subtle-danger. |
size | xs sm md lg. |
block, align | Full width; align = 'start' left-aligns the content. |
event | Name of the event sent to Lua (default click). |
submit | true: validate every visible field first; if any fails, nothing is sent. The event then carries values. |
close | true: close the window after sending. |
args | Any 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. |
toggle | A 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:
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
menu renders a navigable list. See menus. Most callers use tm.ui.menu rather than writing the node.
Validation rules
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 (
whenis false) are skipped. patternis a JavaScript regular expression source.messagereplaces 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 asuser.name. - Arrays are values, not paths.
- An empty Lua table becomes
[]in JSON. The page treats an emptyvaluesas "no values"; usefalse(notnil) for a value you want to keep, becausenilfields 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:
| Write | Get |
|---|---|
**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.
{ 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').
{ 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(andborderTop/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 (
styleon 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:,!importantand 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
styleon arowchild affects the wrapper.