# Game UI widgets

> Define reusable UI components with typed props and slots once, and use them as tags in any screen, template or script markup.

Source: https://feather-engine.com/docs/presentation/game-ui-widgets/

A widget is a piece of [game UI](/docs/presentation/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`.

```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>
```

## Built-in widgets

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`.

## Use a widget

- **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`.

## Make a widget

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.

```json
{
  "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>"
}
```

### Rules

- 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](/docs/presentation/game-ui/#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.

## Limits

| 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 |

## Automate it

| 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:

```bash
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](/docs/agents/overview/) for connecting a live session.
