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.
Create a screen
Section titled “Create a screen”- Click Settings in the toolbar and open Game UI screens.
- Type a name and click New screen. Names use lowercase letters, digits,
-and_. - Write the HTML and CSS, and describe the screen and the fields it reads in Description.
- Enter Sample data as JSON. The preview renders the screen with it.
- Set the Layer, from −100 to 100. Higher layers draw on top.
- 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.
Markup
Section titled “Markup”| 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 ….
Data bindings
Section titled “Data bindings”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>Lists that animate
Section titled “Lists that animate”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.
Button events
Section titled “Button events”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.
The project stylesheet and fonts
Section titled “The project stylesheet and fonts”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 UI kit
Section titled “The UI kit”The engine ships ready-made classes you can use in any screen, widget or ui_html markup.
Motion
Section titled “Motion”| 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 |
Enter and exit
Section titled “Enter and exit”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>Screen effects
Section titled “Screen effects”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.
Layout and components
Section titled “Layout and components”| 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 |
Script API
Section titled “Script API”Screens
Section titled “Screens”| 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");}Screens on objects
Section titled “Screens on objects”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);}Script elements and HUD
Section titled “Script elements and HUD”| 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);}Built-in screen templates
Section titled “Built-in screen templates”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 |
Pause menu
Section titled “Pause menu”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.
Your own title, pause and options screens
Section titled “Your own title, pause and options screens”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"); } }}Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Screens per project | 64 |
| HTML per screen | 64 KiB |
| CSS per screen | 32 KiB |
| Layer | −100 to 100 |
Automate it
Section titled “Automate it”| 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 |
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.