Appearance
HUD reference
Everything you can put in a menu, panel, prompt or heads-up display.
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.
luau
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.
luau
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. Text and labels come second, by position — not as a text = key.
luau
{ "text", "Hello" } -- correct
{ "text", text = "Hello" } -- draws NOTHINGThis is the single most common HUD mistake. A text = key on a text node is ignored, the node is created empty, it takes up space, and nothing appears. If a label is missing, check that it is in position two.
text
luau
{ "text", "Coins: {coins}", size = 18, bold = true, align = "center" }| Key | Meaning |
|---|---|
size | font size. Default 16, clamped 6–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
luau
{ "button", "Buy rope", emit = "buy", data = { item = "rope" } }| Key | Meaning |
|---|---|
style | "primary" (default), "ghost" |
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 | — |
Navigation and variable changes happen on the player's machine, instantly. Only emit costs a message.
toggle
luau
{ "toggle", "Sound", var = "sound_on" }A checkbox bound to a variable.
slider
luau
{ "slider", var = "volume", min = 0, max = 100, step = 5 }progress
luau
{ "progress", var = "health", max = 100, color = "#39d98a" }A horizontal bar.
ring
luau
{ "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
luau
{ "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
luau
{ "icon", "heart", size = 20, tint = "#ff5566" }image
luau
{ "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 — 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.
luau
{ "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, not 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, 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 — 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 |
luau
{ "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.
lua
{ "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 — 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.
luau
{ "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.
separator / spacer
luau
{ "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 – 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
luau
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
luau
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:
luau
"#141a24" -- what every colour picker gives you
"#141a24cc" -- with transparency
"navy" -- a name: red, white, transparent…
{ 20, 26, 36 } -- r, g, b from 0 to 255Motion
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" |
effect_amount | |
effect_speed |
Conditions
visible and a button's enabled take the same notation:
luau
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.
luau
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 | the text was given as text = instead of in position two |
| 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
- show_hud
- self:on_hud — receiving a button
- Menus and HUDs — worked examples
