Skip to content

Game UI widgets

A widget is a piece of game UI you define once and use anywhere as a tag: a stat bar, an item slot, a modal dialog. It has its own markup and CSS, props (typed inputs with defaults) and slots (places the screen fills with its own content). Widgets work everywhere markup does: screens, built-in screen templates and ui_html.

<div class="top-left stack" style="width: 280px;">
<stat-bar label="Health" icon="heart" value="{{hp}}" max="{{hp_max}}" tone="bad"/>
<stat-bar label="Stamina" icon="lightning" value="{{stamina}}" tone="good" show_value="false"/>
</div>
<modal-dialog title="Paused" close="resume" exit="300ms">
<p>The world waits.</p>
<icon-button slot="footer" label="Resume" variant="primary" onclick="resume"/>
</modal-dialog>

The engine ships eight widgets. A project widget with the same name replaces one.

Widget Props Slots
stat-bar label, value (100), max (100), icon, tone, show_value (true) —
icon-button label, icon, onclick, variant (default, primary, ghost, danger), hotkey, disabled Default, after the label
item-slot icon, image (a project texture), count (0 hides it), hotkey, rarity (common, uncommon, rare, epic, legendary), selected, onclick —
modal-dialog title, subtitle, icon, close (the X button’s action; empty hides it), width (480 px) Default (the body), footer
key-hint cap (the key’s text, default E), label, hold —
tab-bar tabs (a list of {id, label, icon}), active (the current tab’s id), onclick (default tab) —
notice-card icon (default info), title, text, tone Default
progress-ring value (0), max (100), label, size (84 px), tone —

tone is accent (the default), good, bad or info. Icons are Phosphor icon names. An item-slot without onclick is not a button, so a HUD hotbar keeps the mouse captured. A tab-bar click sends its onclick action with the clicked tab’s id as tab.

  • Props are attributes. Each value is converted to the type of the prop’s default: numbers (value="{{hp}}"), booleans (a bare attribute such as selected is true, "false" is false), and lists or objects as JSON (tabs="{{json tabs}}"). A missing or empty attribute keeps the default.
  • Children fill slots. Plain children go to the default slot; a child with slot="footer" goes to the footer slot.
  • The root takes the tag’s class, style, id, key, exit and data-* attributes, plus the widget’s name as a class. So <stat-bar class="fx-slide-left" key="{{id}}"/> animates and keeps its element in a keyed list, and .stat-bar styles every instance.
  • Clicks. A widget’s buttons take their action from a prop, so the screen decides the event: <icon-button onclick="buy" data-item="{{id}}"/> sends ui:buy with item.

Unknown props, wrong types and values outside a prop’s options are reported as warnings, and the prop keeps its default. The warnings appear in the editor, in ui.render and in ui_widget.render.

  1. Click Settings in the toolbar and open Game UI widgets. The list shows built-in widgets and your own; customised built-ins are marked.
  2. Type a name and click New widget. Names are lowercase words joined by hyphens, such as shop-item; the hyphen is required, so a widget never clashes with a built-in tag.
  3. Fill in the Description, add props with + Prop (name, default, options, description), and write the HTML and CSS.
  4. Write an Example usage. The preview renders it and lists warnings and button events; turn on Show in viewport to see it with the real game renderer.
  5. Click Apply. Revert discards your edits.

To change a built-in, select it and click Customise: the project gets a copy you can edit, and Reset to built-in drops your changes. A widget of your own is removed with Delete, which is refused while a screen, template or widget still uses it.

{
"name": "shop-item",
"description": "A row in a shop: icon, name, price and a buy button.",
"props": {
"name": {"default": "Item", "description": "Item name"},
"price": {"default": 0, "description": "Price in gold"},
"gold": {"default": 0, "description": "The player's gold"},
"icon": {"default": "package"},
"rarity": {"default": "common", "options": ["common", "rare", "epic"]},
"onclick": {"default": "buy"}
},
"html": "<div class=\"card rarity-{{rarity}}\"><div class=\"spread\"><div class=\"inline\"><item-slot icon=\"{{icon}}\" rarity=\"{{rarity}}\"/><div class=\"stack\"><p>{{name}}</p><slot/></div></div><icon-button label=\"{{price}} g\" icon=\"coins\" variant=\"primary\" onclick=\"{{onclick}}\"{{#lt gold price}} disabled{{/lt}}/></div></div>",
"css": ".shop-item { width: 380px; } .shop-item.rarity-epic { border-color: #b46bff; }",
"example": "<shop-item name=\"Iron sword\" price=\"120\" gold=\"80\" rarity=\"rare\" icon=\"sword\"><p class=\"label\">+4 attack</p></shop-item>"
}
  • The HTML renders to one root element. It reads props with the same bindings and helpers as screens: {{label}}, {{#flag}}…{{/flag}}, {{pct value max}}, {{#eq a b}}…{{/eq}} (see Data bindings).
  • <slot/> is replaced by the tag’s children and <slot name="footer"/> by the children with slot="footer". A slot’s own children are its fallback: <slot name="title">Untitled</slot>. {{#slots.footer}}…{{/slots.footer}} shows a part only when that slot is filled.
  • A prop’s type is the type of its default: text, number, boolean, list or object. options limits a text prop to a set of values, and the default must be one of them.
  • Prop names are lowercase letters, digits and _, and cannot be class, style, id, key, exit, slot or slots.
  • Widgets can use other widgets, up to 12 levels deep. A widget that uses itself is an error.
  • Scope the CSS with the widget’s name (.shop-item …). Widget CSS loads after the UI kit and before the project stylesheet and the screens, so a screen can restyle a widget.
Limit Value
Project widgets 64
Props per widget 32
HTML, CSS 32 KiB each
Description 2,000 characters
Example 8 KiB
Name 48 characters
Nesting 12 levels
Elements one screen expands to 20,000
Command Purpose
ui_widget.get Lists every widget, built-in and project, with its props (type, default, options, description), slots and example. With name, also its HTML, CSS, the expanded example and used_by
ui_widget.upsert Creates or changes a project widget; fields you omit keep their value. A built-in’s name makes an override that starts from the built-in. rename_from renames a project widget and updates its tags in screens, widgets and templates
ui_widget.remove Deletes a project widget; refused while something uses it, unless it overrides a built-in, which then comes back
ui_widget.render Expands a widget (name with optional props and children, or name alone for its example) or any markup, without saving. Returns the markup, warnings and buttons

Save the JSON above as shop-item.json, then create the widget and preview it with other props:

Terminal window
feather-cli --connect .feather-session.json command ui_widget.upsert --json - < shop-item.json
feather-cli --connect .feather-session.json command ui_widget.render \
--json '{"name":"shop-item","props":{"name":"Bow","price":50,"gold":80},"children":"<p>Ranged</p>"}'

ui_widget.render lists one button with onclick buy. A --verify-run report lists screens whose widgets did not expand under ui.widget_errors.

See Agents & automation for connecting a live session.