HUD reference#
Everything you can put in a menu, panel, prompt or heads-up display.
Use it to
- build a menu, a panel or a heads-up display out of plain tables
- show text, numbers, bars, icons and pictures
- take input: buttons, toggles, sliders, text boxes
- style it once with classes and a theme
- float a panel in the world instead of on the screen
At a glance#
What you want done, and the verb that does it.
Show something
Take input
Lay it out
Most HUDs do not need this page
A HUD can be drawn in the editor's Interface tab, saved with the world and shown by name, as in player:show_hud("shop"). See Menus and HUDs. This page is the other way: a HUD written as a table inside a script.
A HUD is a table you hand to show_hud. It is drawn and driven on the player's own machine: changing pages, moving a slider and ticking a box are instant and never touch your script. Only a button carrying emit reports back.
p:show_hud({ id = "shop", anchor = "center", modal = true, vars = { coins = 40 }, pages = { main = { { "text", "General store", size = 22, bold = true }, { "text", "Coins: {coins}" }, { "button", "Buy rope", emit = "buy", data = { item = "rope" } }, { "button", "Close", close = true }, }, },})The outer table#
| Key | Required | Meaning |
|---|---|---|
id | yes | names this HUD. Showing another with the same id replaces it |
pages | yes | named pages; each is a list of nodes |
start | which page opens first. Defaults to main | |
anchor | where it sits: "center", "top", "bottom", "top_left", "top_right", "bottom_left", "bottom_right"… | |
size | { w, h }. 0 = fit the content. "50%" = a fraction of the screen | |
modal | true dims the world behind it | |
theme | colours and shape. See Theme | |
styles | named style classes. See Classes | |
vars | starting values for {placeholders} and for widgets | |
world | hang it in the 3D world instead of on the screen. See World HUDs |
Variables#
vars is the HUD's own little memory. Any text node can interpolate one with braces, and widgets read and write them.
vars = { coins = 40, sound_on = true },pages = { main = { { "text", "Coins: {coins}" }, { "toggle", "Sound", var = "sound_on" },} },Push new values from your script with update_hud. That is how health, a score or a timer stays live without rebuilding the menu.
Nodes#
Every node is a list whose first entry is its kind. Its text or label comes second, by position, or under text =, which means the same:
{ "text", "Hello" }{ "text", text = "Hello" } -- the same node{ "button", label = "Buy", emit = "buy" }text#
{ "text", "Coins: {coins}", size = 18, bold = true, align = "center" }| Key | Meaning |
|---|---|
size | font size. Default 16, clamped 6 to 120 |
color | text colour |
align | "start" (default), "center", "end" |
bold | |
wrap | let long text run onto more lines |
label is the same node under another name.
button#
{ "button", "Buy rope", emit = "buy", data = { item = "rope" } }| Key | Meaning |
|---|---|
style | "primary" (default), "ghost" (bordered, no fill), "danger", "custom" (only your own color and text_color, nothing from the theme) |
color | fill colour |
text_color | |
height | default 42 |
enabled | a condition. The button greys out when it is false |
tag | names the button, if you need to address it |
What a button does, one or more of these:
| Key | Effect | Reaches your script? |
|---|---|---|
goto | open another page | no |
back | return to the previous page | no |
close | close the HUD | no |
set | write a variable | no |
add | add to a numeric variable: add = { "coins", -10 } | no |
emit | report to self:on_hud | yes |
data | the fields that go with emit | none |
Navigation and variable changes happen on the player's machine, instantly. Only emit costs a message.
toggle#
{ "toggle", "Sound", var = "sound_on" }A checkbox bound to a variable.
slider#
{ "slider", "Volume", var = "volume", min = 0, max = 100 }Writes the variable as the player drags. min defaults to 0 and max to 1.
input#
{ "input", "Your name", var = "name", max_len = 24, emit = "named" }A line of text the player types into, bound to a variable. The second entry is the placeholder shown while it is empty.
| Key | Meaning |
|---|---|
var | required. The variable the text is written to |
max_len | longest text accepted. Default 64, at most 256 |
height | default 42 |
enabled | a condition, as on a button |
Pressing Enter runs the same actions a button has, so emit is how the text reaches your script.
select#
{ "select", "Team", var = "team", options = { "Red", "Blue", "Green" }, emit = "team_picked" }A list to pick one from, bound to a variable.
| Key | Meaning |
|---|---|
var | required. Receives the chosen text |
options | required. Two to 32 pieces of text, each up to 96 bytes |
height | default 66 |
enabled | a condition, as on a button |
Changing the choice runs the same actions a button has.
progress#
{ "progress", var = "health", max = 100, color = "#39d98a" }A horizontal bar.
ring#
{ "ring", var = "grab", max = 1, size = 92, thickness = 6, color = "#7dd3fc", track = "#1f2937", start = 0 }The round counterpart of progress. start is where the arc begins, as a fraction of the circle.
hold#
{ "hold", key = "e", dur = 1.4, var = "grab", emit = "pick_up", data = {} }Draws nothing. It drives var from 0 to 1 while the key is held, which is what a ring beside it shows.
| Key | Meaning |
|---|---|
key | a single letter, or "space" |
dur | seconds to complete |
var | the variable it drives, 0 → 1 |
emit / goto / close | what completion does |
data | fields for emit |
on_start / on_complete / on_cancel | extra actions at each moment |
emit is the only part that reaches the server, and it arrives at on_hud with the player who did it.
icon#
{ "icon", "heart", size = 20, tint = "#ff5566" }image#
{ "image", "coin", fit = "contain", w = 32, h = 32, rounding = 4 }The second entry is either a built-in icon name or the id of a texture you uploaded.
Built-in icons: coin, heart, star, check, close, plus, minus, chevron_right, chevron_left, chevron_up, chevron_down, lock, play, pause, circle, ring, square.
| Key | Meaning |
|---|---|
fit | "contain" (default), "cover", "stretch" |
tint | recolours it, which is good for icons |
w / h | size |
rounding | rounded corners |
stage#
A small 3D scene, drawn on this player's machine and nowhere else. This is how you build a lobby: a set behind the menu, with the player's character and their weapon standing in it.
{ "stage", tag = "lobby", h = 420, show = { "world/sets/lobby" }, look = { 0, 1.6, 0 }, dist = 6 },It is a picture, not a place#
Nothing on a stage exists in the world. It is not replicated, it has no physics, and no other player sees a trace of it, so a menu costs the server exactly nothing, however many people are looking at one.
That is what makes a lobby possible. Built out of world objects, a lobby forces you to choose between everybody standing in the same room and building one room per player. A stage removes the question: there is no room.
What it is not is somewhere the player can walk. A place people move around in is a world, not a stage.
What stands on it#
show is a list. A bare name is a piece at the origin; a table places it.
| Key | Meaning |
|---|---|
model | a path on the world's library (world/…). Can be the first entry instead |
who | a person stands here, by their player.id. Not with model |
pose | what that person is doing: an animation asset. Only with who |
speed | how fast pose plays. Default 1, and it may be negative |
pos | { x, y, z } in the stage's own space. Default { 0, 0, 0 } |
rot | { x, y, z } in degrees, like everywhere else |
scale | { x, y, z }, or one number for all three. Default 1 |
A piece is a model or a who, never both. The two readings of { model = …, who = … } are equally sensible (a person in a costume, scenery labelled with an owner), so writing both is refused instead of guessed at.
A path may name either kind of thing on the shelf:
- a model you uploaded, one piece, placed where you put it;
- a set you built, anything you assembled in the editor, took, and published to the shelf. It arrives with all its parts, each one where you left it, and its root lands on the piece's
pos. This is the ordinary way to make a lobby: build it, take it, name it.
Blocks in a set are drawn from their own shape and colour, so a set made in the editor needs nothing uploaded at all.
Where the camera stands#
Without look the camera frames whatever the pieces add up to and fills the rectangle, a backdrop rather than a photograph of a model. With look you place it yourself, which is what a lobby usually wants: standing inside the room, aimed at where the character is.
| Key | Meaning |
|---|---|
look | { x, y, z } the camera aims at, in the stage's space. Default: the middle of the set |
dist | how far back it stands, in metres. Default: far enough to fill the frame |
yaw / pitch | the angle it stands at, in degrees. Default 35 and 12 |
zoom | multiplies whichever distance is in force. Below 1 = closer |
spin | may the player drag to turn it? Default true, or false once you set look. A shot you framed is not a turntable, and dragging one walks the camera through your own walls |
Find the numbers by looking, not by guessing
look is a point in the set's own space, which is a space you have never seen a coordinate of, so typing numbers, saving, and looking again is a slow way to find a shot, and the usual first result is a camera standing outside the building, framing the roof.
Open Dev → Stage framing in the dock while the menu is on screen. It takes the stage in hand: drag it to turn it (even with spin off), move the aim and the distance while watching, set the light, and press Copy for the script, what lands on the clipboard is these exact lines, ready to paste into your stage{ … }. Closing the window hands the stage straight back to the script; nothing it does outlives it.
What it looks like#
A stage has no background of its own: whatever is behind it in the HUD, your panel, its gradient or an image, shows through, so put the stage at the bottom of a stack and lay the menu over it.
It is drawn with the same materials the world draws: base colour, normal, metallic-roughness and emissive maps, cut-outs and blended surfaces, plus contact occlusion in the corners and a glow around anything bright enough to be a light. A set looks in the menu the way it looks standing in it.
It is lit like a place, not a studio: one key light fixed in the set (so turning it does not swing the sun around), parts casting shadows on each other, and no studio backdrop or floor disc. It is deliberately not lit by the world's sun, because a menu should not go dark when night falls in the match.
The light#
Four numbers, and leaving them out is the default rig.
| Key | Meaning |
|---|---|
light.yaw | where the key comes from, in degrees around the set |
light.pitch | and how high above the horizon. 90 is straight overhead, which lights every vertical surface the same and leaves the set with no shape |
light.bright | the key's strength. 1 = the default |
light.ambient | how much of the environment reaches the set. Turn it down for night: what is left is the key and whatever the set itself emits |
{ "stage", show = { "world/sets/lobby" }, look = { 0, 1.6, 0 }, dist = 6, light = { yaw = 120, pitch = 30, bright = 1.4, ambient = 0.35 } },A set is a mood, and which mood is yours: the same warehouse is a morning depot or a night staging area depending on these four.
When something cannot be drawn#
A stage says so in its own rectangle, naming the path: a name the shelf does not hold, a sound or a texture where a set was meant, a model whose file did not download. It never sits on a loading plate for something that is not coming.
People on a stage#
who puts somebody's avatar in the set: the player, a teammate, whoever your script names. They stand in the scene rather than over it: lit by the stage's own light, hidden behind anything in front of them, and casting onto its floor.
{ "stage", tag = "party", look = { 0, 1.2, 0 }, dist = 3.5, show = { "world/sets/lobby", { who = me.id, pos = { -0.6, 0, 0 }, rot = { 0, 160, 0 } }, { who = friend.id, pos = { 0.6, 0, 0 }, rot = { 0, 200, 0 }, pose = CHEER }, } }Three things worth knowing before you write it:
- Only somebody in this world can be shown. A person who is not here is not drawn, and the stage says so in its own rectangle. Their avatar simply does not exist on this machine.
- The figure is a picture, not a second view of them. Posing it does not touch their body in the world, and nobody else sees what you did, which is why it needs no permission at all, unlike
players:play_animation. pose, notanim.animis already the entrance animation a node plays when it appears. One name, one concept.
A stage with people on it redraws every frame, because a body is never still. One without them costs nothing when nothing changes.
Limits#
At most 4 stages in one HUD, 64 pieces on one stage, and 4 people on one stage, and a set counts as the one piece you wrote, however many parts it has. Over any of them, the HUD is refused with a reason rather than quietly trimmed. See limits.
row / column#
Containers. Their children are the second entry, as a list.
{ "row", { { "button", "−10", add = { "hp", -10 }, style = "ghost" }, { "button", "+10", add = { "hp", 10 } },}, height = 40, gap = 8 },| Key | Meaning |
|---|---|
gap | space between children. Default 10 |
pad | space inside the container |
height | row only. Default 42 |
justify | row only: "start", "center", "end", "between" |
align | column only |
col is short for column.
stack / scroll / grid#
Three more containers, written the same way: children in the second entry.
{ "scroll", { { "text", "A long list…" }, -- more rows}, tag = "log", height = 240 }, { "grid", { { "image", "rope" }, { "image", "lantern" },}, columns = 4, cell_height = 96 },| Container | What it does | Its own keys |
|---|---|---|
stack | children overlap, later ones on top. One with a pos is placed and sized; one without fills the stack as a background | pad |
scroll | a column that scrolls when its children are taller than it | tag (required, so the position survives an update), height (default 240), gap, pad (default 8) |
grid | children fill cells left to right, top to bottom | columns (default 4, at most 12), cell_height (default 120), gap, or gap_x and gap_y, pad |
separator / spacer#
{ "separator" }{ "spacer", h = 12 }Styling#
Universal keys#
Any node accepts these:
| Key | Meaning |
|---|---|
class | one or more named styles from styles |
w / h | pixels, "50%", or "2fr" (a share of a row) |
margin | n, {vertical, horizontal}, or {top, right, bottom, left} |
opacity | 0 to 1 |
place | "start", "center", "end" |
visible | a condition. See Conditions |
On leaf nodes and containers:
| Key | Meaning |
|---|---|
bg | background colour |
bg2 | second colour, making a vertical gradient |
border | a colour, or { colour, width } |
rounding | corner radius |
shadow | |
bg_image | an icon name or texture id, drawn behind the content |
bg_fit | "cover" (default), "contain", "stretch" |
Classes#
styles = { title = { size = 22, bold = true }, card = { bg = "#1b2330", rounding = 12, shadow = true },},pages = { main = { { "text", "Shop", class = "title" }, { "column", { ... }, class = "card" },} },class = "a b" applies several, left to right. A node's own key always wins over its classes.
Theme#
theme = { bg = "#141a24", bg2 = "#1b2330", text = "#e8edf5", accent = "#6d5dfc", border = "#2a3345", rounding = 12, pad = 14, shadow = true, bg_image = "…",},A page may carry its own theme, which inherits from the HUD's and overrides what it names.
Colours#
Four forms, all accepted anywhere a colour is:
"#141a24" -- what every colour picker gives you"#141a24cc" -- with transparency"navy" -- a name, from the list below{ 20, 26, 36 } -- r, g, b from 0 to 255The names: white, black, red, green, blue, yellow, orange, purple, pink, cyan, teal, lime, gold, brown, navy, gray, silver, charcoal and transparent.
Motion#
Pure client-side, costing nothing:
| Key | Meaning |
|---|---|
anim | entry animation: "rise", "fade", "slide_left", "slide_right", "slide_down", "pop" |
anim_dur | seconds |
anim_delay | seconds. Stagger a list by giving each row a bigger delay |
effect | continuous: "float", "pulse", "spin", "orbit" |
effect_amount | |
effect_speed |
Conditions#
visible and a button's enabled take the same notation:
visible = "open" -- is the variable truthy?visible = { "not", "open" }visible = { "==", "mode", "stopped" }visible = { ">", "coins", 10 }visible = { "<", "hp", 25 }World HUDs#
Set world and the panel stops being a screen corner and becomes a billboard hanging on an object in the 3D scene: a "hold E to pick up" prompt, a shop sign, a nameplate.
It always faces the player, is scaled by distance, and disappears when the anchor is out of range, behind the camera, or not being looked at.
p:show_hud({ id = "pick_up", world = { height = 1.2, distance = 3, fade = 9, gaze = 20, }, size = { 100, 100 }, vars = { grab = 0 }, pages = { main = { { "ring", var = "grab", size = 30, thickness = 2 }, { "text", "Hold E", size = 10, align = "center" }, { "hold", key = "e", dur = 1.4, var = "grab", emit = "take" }, } },})world = true takes every default. A table overrides what it names:
| Key | Meaning |
|---|---|
object | which object it hangs on. Defaults to the one running the script |
height | metres above the object |
offset | {x, y, z} instead of height, for full control |
distance | stop drawing past here |
fade | start dissolving here |
gaze | only while looked at, within this many degrees. 0 = any angle |
pixels_at | the distance at which it draws at its authored pixel size. 0 = never scale |
scale | { min, max } |
Common mistakes#
| Symptom | Cause |
|---|---|
| a label is invisible | its colour is the panel's, or its visible condition is false |
| the HUD never appears | id or pages missing, or show_hud called outside a handler |
| a button does nothing | it has no emit, goto, back, close, set or add |
| the number never changes | you rebuilt with show_hud instead of pushing with update_hud |
| a world HUD never shows | the player is outside distance, or outside the gaze cone |
See also#
- self:on_hud receives a button
- Menus and HUDs has worked examples
Updated 17 October 2026