Skip to content

Game UI

Game UI is written in an HTML-like markup and styled with CSS. The same renderer draws your own screens, the engine’s built-in HUD, dialogue box, journal and prompts, so one stylesheet restyles the whole game. UI appears in the editor’s Play mode and in exported games.

There are three ways to put UI on screen:

Approach Use it for
Screens Project-stored HUDs, menus, shops and inventories with data bindings, shown from scripts
Script elements Quick labels, bars, panels and toasts drawn directly from a script
Templates Overriding the markup of the engine’s built-in screens

Widgets are reusable components, such as a stat bar or a modal dialog, that you use as tags in any of the three.

  1. Click Settings in the toolbar and open Game UI screens.
  2. Type a name and click New screen. Names use lowercase letters, digits, - and _.
  3. Write the HTML and CSS, and describe the screen and the fields it reads in Description.
  4. Enter Sample data as JSON. The preview renders the screen with it.
  5. Set the Layer, from −100 to 100. Higher layers draw on top.
  6. Click Show in viewport to see the screen over the game view.

Show the screen from a world script, then update only the data that changes:

fn start() {
ui_show("hud", #{ health: 100, ammo: 30, items: [] });
}
fn on_player_hurt(amount) {
ui_set("hud", "health", player_health());
}

The engine updates the existing widgets in place, so CSS transitions keep running as values change.

Category Supported
Block tags panel, div, section, row, col, h1–h3, p, button, bar, hr, spacer, img, ul/ol/li, table/tr/th/td. Unknown tags act as div
Inline tags b, strong, em, i, span, small, icon, a, br, code
Common attributes class, id, style, title (tooltip), key (see Lists that animate)
Element attributes button id disabled onclick; bar value max label; img src width height (a project texture name); icon name; table striped
Screen attributes exit on the screen’s first element: how long the screen stays to animate out, such as exit="300ms"

Inline tags next to each other become one line of text and have no box of their own, so padding, backgrounds and layout do not apply to them. Use a block tag, such as <div class="badge"> instead of <span class="badge">, whenever you need a box.

<icon name="…"> takes any Phosphor icon name, such as thumbs-up or rocket-launch, plus game aliases such as gold, health, quit and music. Unknown names draw a circle.

The screen’s root element has the screen name as its class, so CSS can scope rules with .hud ….

Screens use a Mustache subset:

Syntax Effect
{{name}} Inserts a value, escaped for markup
{{{name}}} Inserts a value unescaped
{{#list}}…{{/list}} Repeats for each item of a list, or shows once when the value is true, a non-empty string or an object
{{^list}}…{{/list}} Shows only when the value is missing, false or empty
{{a.b}} Reads a field of an object
{{.}} The current item

Helpers compute values and compare them:

Helper Gives
{{pct value max}} value / max as a whole percentage, 0 to 100, for widths and gauges; max defaults to 100
{{angle value max from to}} The degrees (0–360) that value / max fills of the from%–to% part of a round gauge, for rotate
{{round x}} A whole number
{{json x}} The value as JSON, to pass a list or object to a widget prop
{{#eq a b}}…{{/eq}} Shows its content when a equals b; {{^eq a b}} shows it when they differ
{{#lt a b}}…{{/lt}}, {{#gt a b}}…{{/gt}} Numeric less-than and greater-than

Operands are field names, numbers, true or false, quoted text ('gold') or a ratio of two operands (hp/max): {{#lt hp/max 0.25}}<p>Low health</p>{{/lt}}.

<panel class="shop">
<h2>{{title}}</h2>
{{#items}}
<row>
<span>{{name}}</span>
<button onclick="buy" data-item="{{id}}">Buy</button>
</row>
{{/items}}
{{^items}}<p>Sold out</p>{{/items}}
</panel>

Give repeated items a key and each one keeps its element, and any animation it is playing, when the list changes:

<div class="stack">
{{#quests}}<div class="notice fx-slide-left" key="{{id}}"><p>{{title}}</p></div>{{/quests}}
</div>

With keys, a new item animates in on its own and removing one leaves the others alone. Without keys, any change in the number of items rebuilds the list and every item restarts its animation.

A button with onclick="buy" sends the event ui:buy to every script, with a map holding screen and each data-* attribute. It also calls the on_ui(screen, button) hook.

fn start() {
on("ui:buy", |e| {
give_item(e.item, 1);
});
}

Each screen’s CSS joins the project stylesheet. The renderer supports flexbox and grid layout, gradients, transition, @keyframes, CSS variables, :hover, :active, :nth-child, :has(), attribute selectors and @media. Tags are type selectors (panel, button, bar), class becomes .class, id becomes #id, and other attributes become [attr] selectors.

opacity fades an element and everything inside it. translate, scale, rotate and transform move elements, and transition, @keyframes and animation animate them.

The engine theme defines variables at :root: --panel, --panel-strong, --line, --line-strong, --text, --muted, --accent, --accent-bright, --good, --bad and --radius. Override them to re-theme every screen at once.

The game stylesheet is built in this order: the engine theme, the UI kit, widget CSS, the project stylesheet, then each screen’s CSS. Later rules win, so a screen can restyle anything before it. A declaration the renderer does not support is dropped and the rest of the stylesheet still loads; the problem is reported (in the editor, and as ui.css_error in a --verify-run report). When two @keyframes share a name, the later one is used.

Edit the stylesheet in Settings → Game UI stylesheet, or in Window → Game logic → UI stylesheet. Start from the built-in theme copies the default rules so you can edit them. Click Apply to keep your changes.

Click Import font… to add a TTF or OTF font, then use it with font-family: "Name".

The engine ships ready-made classes you can use in any screen, widget or ui_html markup.

Classes Effect
fx-fade-in, fx-slide-up, fx-slide-down, fx-slide-left, fx-slide-right, fx-pop, fx-zoom-in, fx-bounce, fx-wobble, fx-shake, fx-grow-x Play once when the element appears
fx-pulse, fx-float, fx-spin, fx-blink, fx-glow Loop while the element is shown
fx-slow, fx-fast Change the animation length to 0.9 s or 0.2 s
delay-1 … delay-6 Delay the animation or transition in steps of 0.06 s
stagger On a parent: its children animate one after another

Give elements enter-fade, enter-up, enter-down, enter-left, enter-right, enter-pop or enter-zoom and they animate in when the screen is shown. They also animate out when it is hidden, if the screen’s first element has an exit time: the screen then stays on screen that long, up to 10 s, with the class leaving, and its buttons stop responding. On its first frame a screen has the class entering. You can write your own rules for screen.entering and screen.leaving.

<div class="scrim enter-fade" exit="350ms">
<div class="card glass enter-pop" style="width: 480px;">
<div class="card-header"><h2 class="title">Paused</h2><div class="badge">Day {{day}}</div></div>
<div class="menu stagger">
<button class="primary fx-slide-left" onclick="resume">Resume</button>
<button class="fx-slide-left" onclick="settings">Settings</button>
</div>
</div>
</div>

ui_flash(screen, effect) plays a one-off reaction on the whole screen: shake, bounce, pulse, wobble, glow or flash. ui_class(screen, "dim", true) keeps a screen dimmed.

Classes What they do
center, top-left, top-right, top-center, bottom-left, bottom-right, bottom-center, fill Place an element on screen
stack, inline, spread, grid-2, grid-3, grid-4, grow Arrange children in a column, a row, a spaced row, a grid, or fill the space
scrim Dims everything behind a modal
card, card-header A content card and its header row
glass, neon A frosted panel look, and a glowing border
title, subtitle, big-number, label Text styles
badge (with good or bad), chip, divider A pill label, a small rounded tag and a thin line
tabs A row of buttons; give the current one active
toggle, toggle on A switch: <div class="toggle on"><div class="knob"></div></div>
meter, meter-fill A bar whose fill you size, such as style="width: {{pct hp max}}%"
bar.thin, bar.glow Bar variants
menu A column of big buttons that slide when hovered or focused
slot, slot.selected Inventory and hotbar cells
tooltip, notice A small floating label, and a raised message box
Function Description
ui_show(name), ui_show(name, data) Shows a screen, optionally with data
ui_set(name, key, value) Changes one data field
ui_data(name) Returns the screen’s current data
ui_hide(name) Hides a screen
ui_toggle(name) Shows or hides a screen; returns whether it is now shown
ui_shown(name) Whether a screen is shown
ui_attach(name, target), ui_attach(name, target, height) Makes a shown screen follow an object, "player" or a vec3, for health bars, prompts and markers
ui_surface(name, object), ui_surface(name, object, width, height) Draws a shown screen or a ui_html id onto an object instead of over the game view
ui_detach(name) Returns an attached screen to the layout, or takes it off its object
ui_class(name, class, on) Adds or removes a class on the screen, for CSS such as screen.boss …
ui_flash(name, class), ui_flash(name, class, seconds) Adds a class for a while (default 0.6 s, up to 30 s); flashing again restarts it
ui_var(name, var, value) Sets the CSS variable --var on the screen; numbers stay unitless, so pass "40%" or "12px" for units

One screen can be shown many times as instances by adding # and a suffix, such as ui_show("enemy_bar#12", data).

ui_class, ui_flash and ui_var also work on ui_html ids. Class names are 1 to 48 letters, digits, - or _; entering and leaving are reserved for the engine. A variable value is at most 200 characters, without ;, { or }.

fn on_player_hurt(amount) {
ui_set("hud", "health", player_health());
ui_var("hud", "hp", player_health() + "%"); // width: var(--hp) in the screen's CSS
ui_flash("hud", "shake");
}

ui_surface draws a screen onto every mesh of an object as an unlit picture, for computer monitors, scoreboards, clocks and signs. The screen keeps its data bindings and animations. Use a thin box whose front (+Z face) is the screen; its other faces show the picture too. The size is in pixels, 16 to 2,048 on each side, and defaults to 512 × 384. At most 32 surfaces can be bound at once. ui_detach or ui_hide unbinds a surface, and Stop gives the object its own materials back.

fn start() {
ui_show("scoreboard", #{ home: 0, away: 0 });
ui_surface("scoreboard", find("Scoreboard"), 1024, 320);
}
Function Description
ui_label(id, text) A text label
ui_bar(id, label, value, max) A progress bar
ui_panel(id, title, lines, buttons) A panel with lines of text and buttons; clicks call on_ui(panel, button)
ui_html(id, markup) Any markup
html(text) Escapes text before you insert it into markup
toast(text) A short notification
set_hud(visible) Shows or hides the HUD
set_marker(key, x, y, z, label), clear_marker(key) Places or removes a world marker

Script elements are rebuilt every frame. Call them from update(dt) to keep them on screen.

fn update(dt) {
ui_bar("stamina", "Stamina", get("stamina"), 100);
}

Settings → Game UI templates lets you override the markup of the engine’s own screens. Each template receives fixed fields:

Template Shows
dialogue The dialogue box, with speaker, text and choices
subtitle A subtitle line
quest_tracker Active quests and objectives
journal The quest journal
save_menu Save and load slots
skip_hint The cinematic skip prompt
inventory The player’s inventory
container A chest or container
status_bar Health, statuses, the worn tool and stamina

In an exported game, Escape (or the input action bound to pause) opens the pause menu: Resume, Save game, Load game, Settings, Controls and Quit to desktop. Settings covers graphics, fullscreen, vertical sync, music, effects and voice volumes, mouse sensitivity, camera shake, motion blur and language. Controls lets players rebind keys and gamepad buttons. See Saves & settings.

A game can draw its own menus as screens and leave the engine’s menu for settings and key bindings:

Function Description
use_game_menu(on) The game draws its own pause menu: Escape no longer opens the engine’s menu and reaches on_key instead
open_system_menu() Opens the engine’s menu on its Settings page, in an exported game
pause_game(on), game_paused() Freezes game time (physics, timers and the player) while hooks keep running with dt 0, and reads whether it is paused
quit_game() Quits to the desktop; in the editor it stops Play
user_settings() The player’s settings as a map
set_user_setting(key, value) Changes one setting and saves it, as the pause menu’s Settings page does

use_game_menu and pause_game reset when Play stops. user_settings() has these keys:

Key Value
quality "Low", "Medium" or "High"
fullscreen, vsync, motion_blur true or false
master_volume, music_volume, sfx_volume, voice_volume 0 to 2
mouse_sensitivity 0.1 to 4
camera_shake 0 to 1

Numbers outside a range are clamped. An unknown key or a value of the wrong type is a script error.

fn start() {
use_game_menu(true);
on("ui:resume", |e| { ui_hide("pause"); pause_game(false); });
on("ui:options", |e| open_system_menu());
on("ui:quit", |e| quit_game());
// <button onclick="music" data-level="0.5">Quiet music</button>
on("ui:music", |e| set_user_setting("music_volume", e.level));
}
fn on_key(key) {
if key == "ESCAPE" {
let paused = !game_paused();
pause_game(paused);
if paused { ui_show("pause"); } else { ui_hide("pause"); }
}
}
Limit Value
Screens per project 64
HTML per screen 64 KiB
CSS per screen 32 KiB
Layer −100 to 100
Command Purpose
ui.get Lists screens, or returns one screen’s HTML, CSS, description, layer and sample
ui.upsert Creates or changes a screen; fields you omit keep their value, and the sample must render
ui.remove Deletes a screen
ui.render Renders a screen or draft HTML with data and returns its markup and buttons, without changing anything. When the screen uses widgets it also returns widgets, expanded (the markup the game draws) and widget_warnings
ui_widget.get, ui_widget.upsert, ui_widget.remove, ui_widget.render Manage and preview widgets
font.import Imports a TTF or OTF font for the stylesheet
Terminal window
feather-cli --connect .feather-session.json command ui.upsert --json '{
"name": "hud",
"description": "Health bar. Fields: health, max.",
"html": "<panel class=\"hud-health\"><bar value=\"{{health}}\" max=\"{{max}}\" label=\"Health\"></bar></panel>",
"css": ".hud .hud-health { width: 240px; padding: 8px; }",
"layer": 10,
"sample": {"health": 75, "max": 100}
}'

A --verify-run report (see Profiler) has a ui section with the screens on show, the screens playing their exit (leaving), css_error, widget_errors and the screens drawn onto objects (surfaces).

See Agents & automation for connecting a live session.