Coming soon toSteam
Modding docsAPI 1

Interface

Mods build interfaces from layout files (.uxml, XML that lists the elements) and style files (.uss, written like CSS), or entirely from code. Only known elements, attributes and style properties are accepted, and anything else is reported with a suggestion.

Examples: Host Tools (a panel from UXML), Thruster Tuner (rows built in code), Effects Lab (tabs, sliders, dropdowns), and Menu Tweaks (changing the game's menus).

Your own panels

local panel = ui.panel{ anchor = "top-right", margin = 24, width = 280, padding = 12 }
panel:style{ ["background-color"] = "#16181dee", ["border-radius"] = "6px" }

local title = panel:addNew("Label", { text = "Scoreboard", style = { ["font-size"] = "16px" } })
local reset = panel:addNew("Button", { text = "Reset", tooltip = "Start counting again" })
reset:on("click", function() score = 0 end)

ui.panel(options) puts a panel on top of the game (needs ui). Options:

OptionDescription
anchortop-left (default), top, top-right, left, center, right, bottom-left, bottom, bottom-right
marginDistance from the screen edge in pixels (default 16)
width, heightSize in pixels (otherwise it fits its content)
paddingInner spacing in pixels
class, style, nameAs for any element

Panels stay on screen until you :destroy() them or the mod stops.

Layout files

ui/scoreboard.uxml
<UXML>
    <Style src="scoreboard.uss" />
    <VisualElement class="card">
        <Label name="title" class="title" text="Scoreboard" />
        <ScrollView name="rows" />
        <Button name="reset">Reset</Button>
    </VisualElement>
</UXML>
ui/scoreboard.uss
:root { --accent: #f59e0b; }
.card { width: 280px; padding: 12px; background-color: rgba(22, 24, 29, 0.93); border-radius: 6px; }
.title { font-size: 16px; font-style: bold; color: var(--accent); }
Button:hover { background-color: rgba(255, 255, 255, 0.12); }
local panel = ui.panel{ anchor = "top-right" }
local board = ui.load("ui/scoreboard.uxml")
panel:add(board)
board:find("#reset"):on("click", resetScores)

A layout file has one <UXML> element around everything. Text between the tags of a Label or Button is its text, the same as text="…". Elements are matched by name alone, so a prefix such as ui: in front of one is ignored. ui.load builds a fresh copy each time. Style sheets named with <Style src> apply only to that file's elements.

Paths in a .uxml (<Style src>, an Image's src) and in a .uss (url(…)) are relative to the file they are written in. Start with / for the mod root. Paths passed to functions from Lua are always from the mod root.

Elements

VisualElement, Box, GroupBox, ScrollView, Foldout, Label, Button, RepeatButton, Toggle, Image, ProgressBar, HelpBox, TextField, IntegerField, FloatField, Slider, SliderInt, MinMaxSlider, DropdownField, RadioButton, RadioButtonGroup, ListView, TabView, Tab.

Attributes

On every element: name, class, style, tooltip, enabled, focusable, picking-mode.

Depending on the element: text, label, value, low-value, high-value, min-value, max-value, page-size, direction, inverted, show-input-field, max-length, password, multiline, readonly, placeholder-text, hide-placeholder-on-focus, choices, index, src, scale-mode, title, toggle-on-label-click, message-type, fixed-item-height, selection-type, show-border, show-alternating-row-backgrounds, mode, horizontal-scroller-visibility, vertical-scroller-visibility.

From Lua, write them as options in camelCase: { lowValue = 0, highValue = 100, showInputField = true } (low_value and "low-value" work too).

Style properties

Layout: display, position, left top right bottom, width height and their min-/max-, flex, flex-grow, flex-shrink, flex-basis, flex-direction, flex-wrap, align-items, align-self, align-content, justify-content, margin…, padding…, overflow, overflow-clip-box, visibility, opacity. Look: background-color, background-image, background-image-tint-color, border-width, border-color, border-radius and their per-side forms, slice-left/right/top/bottom/scale (nine-slice pictures). Text: color, font-size, font-style (normal, bold, italic, bold-and-italic), text-align (upper-left, middle-center, lower-right …), white-space, text-overflow, letter-spacing, word-spacing, paragraph-spacing, text-outline-color, text-outline-width.

A few more names pass the checks but have no effect on screen: background-position…, background-repeat, background-size, font, font-definition, text-shadow, text-outline, text-overflow-position, cursor, rotate, scale, translate, transform-origin and transition….

Selectors: Type, .class, #name, descendant (a b), child (a > b), groups (a, b), variables (--name / var(--name)), and the states :hover, :active, :inactive, :focus, :selected, :checked, :disabled, :enabled, :root.

Working with elements

local slider = panel:find("#power")          -- #name, .class or Type
slider:set("value", 50)
slider:on("change", function(value) setPower(value) end)

local dropdown = panel:find("#mode")
dropdown:set("choices", { "Calm", "Wild" })
dropdown:set("index", 0)

label:setText("Ready")
button:set("enabled", false)
box:hide()                                   -- or :show(), :set("visible", true)
row:addClass("selected")
for _, child in ipairs(list:children()) do child:destroy() end
MethodDoes
add(child), addNew(kind, options)Put an element inside this one
find(selector), findAll(selector, limit?), children(), parent()Look around
get(property), set(property, value)value, text, label, tooltip, enabled, visible, choices, index (and focused to read)
setText(text), text()The element's text
style{ … }, addClass, removeClass, hasClassStyling
show(), hide(), focus()Visibility and focus
on(event, handler)click, change (with the new value), input (while typing), focus, blur, enter and leave (the pointer)
remove()Take out of the interface. A game element comes back when the mod stops.
destroy()Remove for good (your own elements only)

The game's own screens

With the ui.game permission a mod can change the game's screens. Every change is recorded and undone when the mod stops.

Screen idWhat it is
main-menuThe main menu: play, maps, multiplayer, settings
pause-menuThe pause menu shown with Escape
buildingThe building editor: toolbar, block palette, panels
vehicle-hudInstruments while driving or flying a creation
interaction-promptThe "press E to …" prompt
planet-hudAltitude and gravity readout on planet maps
hangarSaved creations
-- Edit
local subtitle = ui.find("main-menu", ".brand-subtitle")
if subtitle then subtitle:setText("Now with mods") end

-- Add
local screen = ui.screen("pause-menu")
if screen then screen:add(ui.element("Button", { text = "My mod's options" })) end

-- Remove
local about = ui.find("main-menu", "#btn-about")
if about then about:remove() end

Screens are built when they open and thrown away when they close. Apply your changes in events.on("ui.screen.opened", fn), where fn receives { id = "main-menu" }, and once at start for screens already open (ui.screens() lists them with open = true). Check before adding, so reopening does not add a second copy.

To find element names, read the Menu Tweaks example, or list a screen's elements while it is open, from the script editor's Console in a mod with the ui permission:

for _, e in ipairs(ui.findAll("main-menu", "Button")) do log.info(e.name, e:text()) end

To add a button to the building toolbar, use building.addTool (only building needed). See Blocks and building.

Messages

notify.toast(text) shows a short message that disappears by itself. notify.message(text) shows one the player has to dismiss. Both need ui and are rate-limited.

The cursor

In play the mouse steers the camera. A window that needs clicks must free the cursor, then give it back:

cursor.free(true)    -- when the window opens
cursor.free(false)   -- when it closes

The cursor comes back automatically if the mod stops.

Keys

With the input permission:

local binding = input.bind("F7", function() toggleWindow() end)
input.bindRelease("LeftShift", function() boost(false) end)
if input.held("LeftShift") then … end
binding:off()

A mod only receives the keys it binds, never everything the player types. Bindings are silent while the player types in a text field (input.typing() tells you). Escape cannot be bound.

Key names: A–Z, Digit0–Digit9, F1–F12, Space, Enter, Tab, Backspace, Delete, Insert, Home, End, PageUp, PageDown, Up, Down, Left, Right, LeftShift, RightShift, LeftCtrl, RightCtrl, LeftAlt, RightAlt, Minus, Equals, LeftBracket, RightBracket, Semicolon, Quote, Comma, Period, Slash, Backquote, Backslash, Numpad0–Numpad9, NumpadPlus, NumpadMinus, NumpadMultiply, NumpadDivide, NumpadEnter, MouseLeft, MouseRight, MouseMiddle, MouseBack, MouseForward. Common spellings like f7 and space are accepted. Binding a key the game uses works, and logs a note about the clash.

Keep the key in a "type": "key" setting rather than in the script, so it can be changed without editing code, and rebind in mod.settings.onChanged.