> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/ui/uidoc-quick-start.md).

# UIDoc Quick Start

***

UIDoc lets you describe game UI with HTML and CSS, then supply its changing data and handle its events in CSL. It is useful for menus, inventories, skill trees, and other structured screen-space interfaces.

UIDoc is browser-like, but it is not an embedded web browser. Use the supported subset described in [UIDoc HTML and CSS Support](/ui/uidoc-html-css-support.md).

## Create a UIDoc asset

Create a folder under your game's `res` directory. The folder name must end in `.uidoc` and contain `index.html` and `index.css`:

```
res/
└── UI/
    └── settings.uidoc/
        ├── index.html
        └── index.css
```

Asset paths used by CSL are relative to `res`, so this document is loaded as `UI/settings.uidoc`.

## Edit visually and generate CSL

Select a UIDoc asset in the All Out editor to open its visual authoring view. You can select and drag elements on the artboard, resize them, reorder or reparent them in Layers, and edit common HTML and CSS properties in the inspector. The preview is rendered by the UIDoc runtime itself, so it uses the same supported layout and rendering behavior as the game.

The Interface inspector infers dynamic fields, repeated lists, inputs, and actions from the document markup. Saving an asset such as `ui/shop.uidoc` also maintains `scripts/generated/uidoc/ui/shop.csl`. Its typed wrapper supplies `default_data`, `decode_event`, input readers, and `draw`, so game code does not need to repeat the low-level bindings shown later in this guide:

```csl
data := UiShop_UIDoc.default_data();
data.title = "Store";
UiShop_UIDoc.draw(data, player, shop_event);
```

The generated code uses the same public `UI.uidoc_*` calls documented below; it does not add a separate runtime system or change the UIDoc runtime API.

When using the automation tools, `uidoc_create_asset` returns its template version and the exact generated `index.html`/`index.css` contents. UIDoc assets do not hot reload in a running game, so restart the game after changing those files. Use `uidoc_diagnostics` for compile and viewport checks, then `uidoc_runtime_inspect` to inspect live nodes, bindings, styles, click payloads, and rectangles.

## Write the document

In `index.html`:

```html
<div class="screen">
  <div class="panel">
    <span class="title">Settings</span>
    <span class="message">{{message}}</span>
    <button class="close" data-on-click="event:close">Close</button>
  </div>
</div>
```

`{{message}}` is a text binding. `data-on-click` sends a `UIDoc_Event` to CSL.

In `index.css`:

```css
.screen {
  position: fixed;
  inset: env(safe-area-inset-top) env(safe-area-inset-right)
         env(safe-area-inset-bottom) env(safe-area-inset-left);
  display: flex;
  align-items: center;
  justify-content: center;
}

.panel {
  display: flex;
  flex-direction: column;
  width: 420px;
  padding: 24px;
  gap: 16px;
  color: white;
  background: #172033;
  border: 2px solid #52627d;
  border-radius: 12px;
  box-shadow: 0 12px 28px 0 #00000066;
}

.title {
  font-size: 32px;
  text-align: center;
}

.close {
  height: 48px;
  background: #3559a8;
  border-radius: 8px;
}

.close:hover {
  background: #4770ca;
}

.close:pressed {
  background: #29447f;
}
```

Safe-area insets keep full-screen UI clear of device cutouts and the game top-bar area.

## Bind and draw it from CSL

Draw player UI from that player's `ao_late_update` call stack. Clear and rebuild the UIDoc bindings before drawing the document each frame:

```go
settings_uidoc_event :: proc(event: UIDoc_Event, userdata: Object) {
    player := userdata.(Player);
    if player == null return;

    if event.handler == "event:close" {
        player.settings_open = false;
    }
}

draw_settings :: proc(player: Player) {
    UI.uidoc_clear_bindings();
    UI.uidoc_bind_text("message", "Changes are saved automatically.");

    document := get_asset(UIDoc_Asset, "UI/settings.uidoc");
    if document == null return;

    UI.uidoc(document, false, player, settings_uidoc_event);
}

Player :: class : Player_Base {
    settings_open: bool;
    pet_name: string;

    ao_late_update :: method(dt: float) {
        if this.is_local_or_server() && this.settings_open {
            draw_settings(this);
        }
    }
}
```

The callback receives the exact handler string from `data-on-click`. A non-repeated control can supply a static `data-key` for `event.key`; repeated controls use their resolved `data-for-key` or list-item key.

`userdata` is passed only to that callback; it does not identify the document. Each UIDoc asset may be drawn once per simulation submission. When a document stops being drawn, its current activation closes; drawing it again automatically assigns a new activation generation so stale replicated layouts cannot attach to the reopened document. At most four different UIDoc assets may be active at once.

Available top-level bindings are:

```go
bind_player_fields :: proc(player: Player) {
    UI.uidoc_bind_bool("visible", true);
    UI.uidoc_bind_text("name", player.get_username());
    UI.uidoc_bind_float("cameraSize", player.camera.size);
}
```

## Conditions, paint bindings, and lists

Use `data-if` to include a node only while a top-level boolean binding is true. An optional leading `!` inverts it. `data-if` does not resolve list-local expressions such as `item.visible`; repeated visual state should use a list-local paint binding instead, while repeated structural or interactive visibility should be decided when building the CSL list. For other dynamic visual state, keep classes static and bind a supported color or opacity:

```html
<div
  class="notice"
  data-if="showNotice"
  data-style-color="noticeColor">
  {{noticeText}}
</div>
```

Bind `noticeColor` with `UI.uidoc_bind_text` using a supported CSS color string. Class attributes are compiled as static class tokens and do not support `{{...}}` interpolation.

Use `data-for` for repeated data:

```html
<div class="inventory">
  <button
    class="item"
    data-for="item in items"
    data-for-key="item.id"
    data-key="inventory-item"
    data-style-color="item.rarityColor"
    data-on-click="event:item">
    {{item.name}}
  </button>
</div>
```

Build that list in CSL before calling `UI.uidoc`:

```go
Inventory_Row :: struct {
    id: string;
    name: string;
    rarity_color: string;
}

bind_inventory_rows :: proc(items: []Inventory_Row) {
    UI.uidoc_begin_list("items");
    for item: items {
        UI.uidoc_list_item(item.id);
        UI.uidoc_list_bind_text("id", item.id);
        UI.uidoc_list_bind_text("name", item.name);
        UI.uidoc_list_bind_text("rarityColor", item.rarity_color);
    }
    UI.uidoc_end_list();
}
```

Use a stable, unique direct expression such as `data-for-key="item.id"` for each repeated item. It is returned as `event.key`. Give `UI.uidoc_list_item(...)` the same stable value so interaction, input, and scroll identity survive list changes. A static `data-key` names the control's role; UIDoc combines that role with each enclosing list item's bound key and current index for its registered runtime identity.

For the example above, a live test name contains a suffix such as `inventory-item#potion-42:7/__widget`. Inspect the exact name in `client_ui_tree`, then target that instance with the full name or a sufficiently precise suffix such as `Test.click_button("inventory-item#potion-42:7")`. A role-only lookup such as `Test.click_button("inventory-item")` is ambiguous when several rows are visible. `data-on-click` and visible text are not test selectors. Nested lists append one `#key:index` pair per loop.

## Inputs

Bind an input with `data-bind-value`:

```html
<input
  id="pet-name"
  class="name-input"
  placeholder="Pet name"
  data-bind-value="petName"
  data-on-click="event:name-input">
```

Read its current value after the document has been drawn:

```go
draw_pet_name_input :: proc(player: Player, document: UIDoc_Asset) {
    UI.uidoc_bind_text("petName", player.pet_name);
    UI.uidoc(document, false, player, settings_uidoc_event);
    player.pet_name = UI.uidoc_text_value(document, "pet-name", player.pet_name);
}
```

`UI.uidoc_text_value` looks up the input by its `id` or `data-key`, then returns the current value of its `data-bind-value` binding.

## Scrolling and zooming

Scrolling is enabled with CSS overflow. Both axes can be enabled on the same viewport:

```html
<div class="viewport" data-scroll-zoom="zoom">
  <div class="canvas">
    <button
      class="node"
      data-for="node in nodes"
      data-for-key="node.id"
      data-key="canvas-node"
      data-style-transform-x="node.x"
      data-style-transform-y="node.y"
      data-on-click="event:node">
      {{node.name}}
    </button>
  </div>
</div>
```

```css
.viewport {
  width: 100%;
  height: 100%;
  overflow-x: auto;
  overflow-y: auto;
}

.canvas {
  position: relative;
  width: 1600px;
  height: 1000px;
}

.node {
  position: absolute;
  width: 160px;
  height: 64px;
}
```

Bind `zoom` with `UI.uidoc_bind_float`. `data-scroll-zoom` scales the content's positions, sizes, text, images, hit regions, and scroll extents around the viewport center. Keep fixed zoom controls outside the zoomed viewport.

{% hint style="warning" %}
Binding-expression attributes use a direct expression, such as `data-for-key="node.id"`, `data-style-transform-x="node.x"`, or `data-scroll-zoom="zoom"`. Do not put those expressions inside `{{...}}`. Mustache interpolation is for text and image `src`; classes and `data-key` values are static.
{% endhint %}

For a normal scroll panel, omit `data-scroll-zoom`. Dragging pans every enabled axis; the wheel scrolls vertically, or horizontally when only horizontal overflow is enabled.

## Common mistakes

* Load the document using its path relative to `res`, including the `.uidoc` suffix.
* Draw it from local-player UI code under `is_local_or_server()`.
* Call `UI.uidoc_clear_bindings()` and provide the current bindings before each draw.
* Draw each UIDoc asset once per update. Use `userdata` only as callback data.
* Use `data-if` only with top-level booleans. Bind list-local visual state through `data-style-opacity`/`data-style-color`, or omit structural/interactive rows from the bound list.
* Give repeated nodes a stable direct `data-for-key` expression; keep `data-key` static when naming a control role.
* Treat CSS support as property/value specific. `auto` and `none` are accepted only where the generated reference lists them, side borders such as `border-bottom` are unsupported, and `calc(...)` is limited to documented length fields with simple addition/subtraction.
* Give scroll content a real size. Transforms alone should not be used as its only layout size.
* Use UIDoc's own responsive layout and zoom behavior instead of applying a second manual UI scale in CSL.
