Skip to content
Docs
Games

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.

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.

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:

src/menu.ts

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

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:

src/hud.ts

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 for every component and option.