# Menus & HUD

> Build the Menu and the HUD with the retained ui API, and update them with swaps.

A game shows its interface in two places:

- The **Menu** is what players see between matches, and over the game when
  they press Esc. Team selection, settings and the Start button usually live
  here. You build it in top-level code, so it exists as soon as the game
  loads.
- The **HUD** (heads-up display) is drawn over a running match: the score,
  the clock. It belongs to one match, so you build it in the `start` handler.

## Describing UI, not drawing it

You don't draw pixels or write HTML. You describe the interface as a tree of
nodes (stacks, rows, text, buttons, images), and disko builds it in every
player's browser.

You build that tree once. To change part of it later, you send a small
**swap**, such as "replace the score text", instead of rebuilding
everything. This is called a retained UI, and it keeps updates small and fast
for every player.

The building blocks are functions like `ui.stack`, `ui.row`, `ui.text`,
`ui.button` and `ui.image`. Each takes an options object: `children` nests
nodes, and `style` takes `ui.style({ … })`.

## Buttons send requests

Your game runs in the room, not in the players' browsers. So when a player
clicks a button, the click can't run your code directly. Instead, the
browser sends the game a **request**, and your `ui-request` handler decides
what happens.

```mermaid alt="A player clicks Start. The browser sends a request named start to the game in the room. The game's ui-request handler checks it and starts the match; every player's screen updates."
sequenceDiagram
  participant P as Player's browser
  participant G as Your game (in the room)
  P->>G: request "start"
  Note over G: ui-request handler<br/>decides what to do
  G-->>P: the match starts, screens update
```

Because the game decides, a player can't cheat by sending requests the game
doesn't accept: the game is always in charge of its rules.

A control's `on` option says which interaction sends which request:
`activate` (a click or Enter), `submit`, `input`, `change`, `commit` or
`drop`. Your handler receives the player, the request's `event` name and the
`interaction`:

```ts title="src/menu.ts"
ui.menu(
  ui.stack({
    style: ui.style({ gap: 8, padding: 12, background: "surface" }),
    children: [
      ui.text({ text: "Arena" }),
      ui.button({
        label: "Start",
        on: { activate: ui.request({ event: "start" }) },
      }),
    ],
  }),
);

game.on("ui-request", (request) => {
  if (request.event === "start") {
    game.start();
  }
});
```

### Shaping requests

`ui.request` takes options for the cases where a plain click isn't enough:

- `values` sends the control's value or a whole form (`"control"` or
  `"form"`), checked against a `payloadSchema` you define.
- `triggerPolicy` debounces or throttles fast interactions, such as a slider.
- `concurrency` decides what happens if the player clicks again while a
  request is still being handled.
- `requires` only lets players with a permission send it (see
  [Permissions](/games/players-teams-actions#permissions)).

To answer the control that sent a request, call `request.respond(output)`
once. By default the new node replaces the control itself.

## Updating with swaps

To change something on screen, wrap the part that changes in a **slot**
(`ui.slot`), then send a swap that replaces the slot's content. `ui.swap`
describes the change, and `ui.push` sends it to everyone, or
`ui.push(player, swaps)` to one player.

This HUD shows a goal counter that updates when a goal is scored:

```ts title="src/hud.ts"
let goals = 0;
let scoreView: Disko.UiNode | null = null;

function scoreText(): Disko.UiNode {
  return ui.text({ text: `Goals: ${String(goals)}` });
}

game.on("start", () => {
  goals = 0;
  // A slot is a node whose children can be swapped later.
  scoreView = ui.slot({ children: [scoreText()] });
  ui.hud(ui.overlay({ children: [scoreView] }));
});

game.on("crossing", () => {
  goals += 1;

  if (scoreView !== null) {
    ui.push(
      ui.swap({ target: scoreView, method: "children", content: scoreText() }),
    );
  }
});
```

Besides replacing children, a swap can `replace` a node, `append`,
`prepend`, insert `before` or `after`, or `remove` it.

## Local commands

Some interactions don't need the game at all, such as opening a settings
panel. **Local commands** run directly in the player's browser, with no round
trip. For example, a Settings button with
`on: { activate: ui.open(settingsModal) }` opens a modal instantly.

`ui.open`, `ui.close`, `ui.show`, `ui.hide`, `ui.focus` and similar commands
target a node, or `"this"`, `"form"` or `"slot"` relative to the control.

## Showing players

Three components display information about a player that your game never
sees directly:

- `ui.flag({ player })` shows their country flag.
- `ui.connection({ player })` shows their connection quality.
- `ui.playerName({ player })` shows their name, in the color and with the
  badge the room gave them (for example an "Admin" badge).

disko fills these in in each browser, so a game can display a player's
country or connection without having access to it.

## Rules to remember

- Any node can take `requires` to hide or disable it for players without a
  permission.
- HUD nodes belong to their match, and Menu nodes to their game. Using a node
  after its match or game has ended throws a `StaleHandleError`, so don't keep
  HUD nodes around after `stop`.

See the [`ui` reference](/reference/ui) for every component and option.
