Examples
Paste any of these into a client script of a resource that has the ui_page from getting started.
Form with validation and a conditional field
lua
local values = tm.ui.window({
title = 'Create character', icon = 'lucide:user-plus',
children = {
{ type = 'row', children = {
{ type = 'input', path = 'first', label = 'First name', rules = { required = true, minLength = 2 } },
{ type = 'input', path = 'last', label = 'Last name', rules = { required = true, minLength = 2 } },
} },
{ type = 'select', path = 'role', label = 'Role', options = { 'civilian', 'police', 'medic' }, default = 'civilian' },
{ type = 'input', path = 'badge', label = 'Badge', when = { path = 'role', eq = 'police' },
rules = { required = true, pattern = '^[0-9]{3,5}$', message = '3-5 digits' } },
},
footer = {
{ type = 'button', label = 'Cancel', event = 'cancel', close = true, variant = 'ghost' },
{ type = 'button', label = 'Create', event = 'submit', submit = true, close = true, variant = 'primary' },
},
}):wait()
if not values then return end -- dismissed
TriggerServerEvent('chars:create', values) -- validate again on the serverLive settings (events + Lua updating the window)
lua
local win = tm.ui.window({
title = 'Live settings', position = 'right',
values = { volume = 50 },
children = {
{ type = 'slider', path = 'volume', label = 'Volume', min = 0, max = 100, notify = true },
{ type = 'text', id = 'status', tone = 'muted', text = 'Move the slider' },
{ type = 'button', label = 'Reset', event = 'reset' },
},
})
win:onChange('volume', function(d)
SetVolume(d.value)
win:setNode('status', { text = ('Volume %s -> %s'):format(d.old, d.value) })
end)
win:onClick('reset', function() win:set('volume', 50) end)A read-only panel that refreshes (the apiary hive)
Build the schema from data, open it once, then :update it whenever the data changes.
lua
local function buildSchema(view)
return {
title = 'Beehive', icon = 'lucide:hexagon', tone = 'amber', width = 720,
children = { { type = 'row', gap = 16, children = {
{ type = 'stack', grow = 1, children = {
{ type = 'meter', label = 'Health', value = view.health },
{ type = 'badge', tone = 'amber', label = view.honey .. ' honey' },
} },
{ type = 'stack', width = 260, children = {
{ type = 'button', label = 'Feed', event = 'feed', block = true,
disabled = view.syrup < 1, disabledWhen = { path = 'busy', truthy = true } },
} },
} } },
}
end
local win = tm.ui.window(buildSchema(view))
win:on('feed', function()
win:set('busy', true) -- buttons with disabledWhen lock immediately
win:setFocus(false) -- give controls back for the progress bar
if runProgressBar() then
win:setFocus(true)
win:update(buildSchema(fetchView())) -- re-render in place; busy resets with the new schema
else
win:setFocus(true)
win:set('busy', false)
end
end)Menu
lua
local result = tm.ui.menu({
title = 'Garage', pageSize = 6,
options = {
{ label = 'Take out', icon = 'lucide:car', args = 'out' },
{ label = 'Store', icon = 'lucide:parking-square', args = 'store' },
{ label = 'Options', options = {
{ label = 'Auto-lock', checkbox = true, checked = true, onCheck = function(on) SetAutoLock(on) end },
} },
},
}):wait()
if result then print(result.args) endConfirm / alert / prompt / input
lua
if tm.ui.confirm({ title = 'Delete?', text = 'This cannot be undone.', confirm = 'Delete', danger = true }) then ... end
tm.ui.alert({ title = 'Done', text = 'Vehicle stored.' })
local v = tm.ui.prompt({ title = 'Transfer', fields = {
{ type = 'number', path = 'amount', label = 'Amount', min = 1, rules = { required = true } },
}, submit = 'Send' })
local name = tm.ui.input('Rename', 'New name', { required = true })Key events
lua
local win = tm.ui.window({ title = 'Hotkeys', keys = true, children = { { type = 'text', text = 'Press R' } } })
win:on('keydown', function(d) if d.key == 'r' then print('restart') end end)Several windows at once
lua
tm.ui.window({ id = 'list', position = 'left', children = { ... } })
tm.ui.window({ id = 'detail', position = 'right', children = { ... } })
-- Escape closes the topmost closable window. NUI focus is released when the last one closes.Ambient hint card (the tutorial)
A window that never takes focus or the mouse, docked right and slid in:
lua
local function card(step, index, done)
return {
id = 'tutorial:' .. step.id,
eyebrow = 'Getting started', title = step.title,
icon = done and 'lucide:check' or step.icon, tone = done and 'green' or 'blue',
width = 320, position = 'right', animation = 'slide-right',
focus = false, passive = true, closable = false,
headerNodes = { { type = 'dots', total = 5, current = index, done = done or false } },
children = {
{ type = 'text', markdown = true, text = 'Hold [[G]] to open the **radial menu**.' },
{ type = 'wrap', children = {
{ type = 'icon', icon = 'lucide:loader-circle', spin = true, size = 14 },
{ type = 'text', tone = 'muted', text = 'Hold the key to try it' },
} },
},
}
end
local win = tm.ui.window(card(step, 2)) -- slides in
win:update(card(step, 2, true)) -- same id: turns into the "done" state in place
win:close() -- slides outA new id for each step gives the slide between steps; :update keeps the same card. See [player]/tutorial/modules/client.lua.
Dashboard layout
lua
tm.ui.window({
title = 'Profile', width = 640, position = 'top-left',
children = {
{ type = 'avatar', name = 'Jane Doe', subtitle = 'Los Santos PD', badge = 'On duty', badgeTone = 'green' },
{ type = 'grid', columns = 3, gap = 10, children = {
{ type = 'stat', label = 'Arrests', value = '42', tone = 'blue' },
{ type = 'stat', label = 'Fines', value = '$8,120', tone = 'green' },
{ type = 'stat', label = 'Reports', value = '17', tone = 'amber' },
{ type = 'chart', kind = 'line', label = 'Shifts this week', data = { 3, 5, 2, 8, 6, 9, 4 }, span = 2 },
{ type = 'chart', kind = 'bar', data = { { label = 'M', value = 3 }, { label = 'T', value = 5 } }, labels = true, tone = 'green' },
} },
},
})