# UI Layout Editor

> Design a game's HUD visually, style widgets and regions, and preview it on real desktop, tablet, phone and VR screens beside the table's own controls.

- Source: https://diceytable.com/docs/editor/item-editors/ui-layout
- Last verified: 2026-10-13

A **UI layout** is a game's HUD written down as data: the state it reads, and the widgets that
show it. Scripts fill in the state and the platform draws the widgets **off the table** — on the
desktop, on a phone and in VR — so players never have to read status text floating over the
felt. This tab is the visual editor for one layout. The JSON it writes is exactly the JSON your
scripts use; there is no second format.

Open it from the **Game Editor**:

- the Game row's **SCENE UI** block (beside SCENE SCRIPTS) — **Edit** on an attached layout, or
  **+ New UI layout**;
- the Hierarchy's **UI Layouts (N)** folder — select a layout and press **Edit**.

See [the tab strip conventions](https://diceytable.com/docs/editor/item-editors/model#the-tab-strip) — closing,
cycling and mounted-but-hidden tabs apply here too. Unlike most tabs it has no Save button:
every change goes straight into the scene draft, like the
[Material editor](https://diceytable.com/docs/editor/item-editors/material), so **Save Draft** and **Undo** cover it.

## Vocabulary

| Term | Means |
| --- | --- |
| **Layout** | One HUD design: declared state keys plus widgets. Lives in the scene's layout library. |
| **Widget** | One piece of HUD: a status line, an action strip, a prompt, a player panel… |
| **Region** | Where a widget sits on a flat screen (upper, lower-end, center, the question sheet…). |
| **Attachment** | A seat, entity or zone a widget is drawn at (a billboard, and its place in VR). |
| **State key** | A named value a script sets with `ui.set` and a widget reads with `{key}`. |

None of these are engine **Components**, and a layout is not an Entity: it is a record in the
scene document, shared by every variant.

## Library and attachment

Layouts live in a **library** shared by every variant, the same way scripts do — fix a layout
once and every variant that shows it gets the fix. Which layouts a variant shows is its own
choice: the SCENE UI block writes the attachment for the variant you are editing, and the
[variant scope bar](https://diceytable.com/docs/mods/manifest/variants) decides whether that lands in the base or in
this variant only. The usual pattern is a *Scripted* variant that attaches the HUD and an
*Unscripted* one that does not.

A project holds at most **32** layouts and a variant attaches at most **8**. At either limit,
**+ New UI layout**, **Duplicate** and attaching are disabled and the block says why; a new layout
made while its variant is full goes into the library unattached. One layout may be at most
**256 KB** as JSON: an edit, an **Apply JSON** or an import that would pass that is refused with a
message, and nothing is written.

When the game's scripts start, the host **materialises** the attached layouts:

1. every declared default is written (table keys once, seat keys for every seat);
2. the **furniture** widgets are shown — status, actions, player panel, counter, meter, log, hint
   and panel;
3. the **moments** wait for a script — banner, prompt, choice, result, timer and card spotlight appear only when a
   script calls `ui.show("id")` (or `ui.prompt`) — see [Game UI](https://diceytable.com/docs/scripting/concepts/game-ui).

Use a widget's **Condition** to keep furniture hidden until it applies. A widget whose audience is
*actor* is never shown at start (nobody has acted yet); a script shows it from `ui.onAction`.

A layout with an **error** is never shown at the table, and the pack will not publish until it is
fixed. Warnings do not stop anything.

## The three columns

**Layout** (left) — the name and the default font, then the widget palette and the **region
map**: a 3×3 grid for the flat screen, plus **Sheet** (questions: prompts and choices) and
**Attached only** (widgets drawn at a seat, entity or zone). Choose a kind and press
**+ Add widget**; every kind starts valid. Drag a widget between regions, or select it and press
⤵ on the region you want. Widgets move between regions, never to screen positions, so the same
layout fits every screen; the widget's **Style** section sizes, aligns and nudges it within its
region. Under the map, **Region layout** sets how one region lays out its widgets (see
[Region layout](https://diceytable.com/docs/editor/item-editors/ui-layout#region-layout)).

**Canvas** (centre) — the layout drawn by the **real** renderers, at the real size of a real screen.
The frame is the screen in CSS pixels, scaled to fit, and the HUD inside it is laid out exactly as
the table lays it out on that screen: the same desktop, tablet or phone layout, chosen the same way
(a touch screen whose shorter side is under 600 px is a phone), with the same per-device placement
and style.

| Device | What you see |
| --- | --- |
| Desktop 1280×720, Desktop 1920×1080, Laptop 1440×900 | The table HUD as a desktop player sees it |
| Tablet 820×1180, Tablet landscape 1180×820 | The touch layout: the upper region folds into a pill, the side and lower regions stack above the hand, questions become a bottom sheet |
| iPhone 390×844 and 844×390, Android 412×915 and 915×412, Small phone 360×740 and 740×360 | The compact phone layout: the status pill, `upper-end` as a small stack under the dice button, every `actions` widget docked bottom-right, the log behind a bottom-left tab, and questions in a narrower bottom sheet |
| VR tray | The head-tethered tray and the first dialog, as the headset draws them, flattened |

**Table chrome** (on by default) draws the table's own controls in the frame as placeholders, at
the size and place the table draws them on that screen: the room badge, toolbar, connection pill,
utility buttons, dice, player pills and chat feed on a desktop; the menu button and room badge on a
tablet; the menu button, avatar rail, status squares and dice on a phone; and the hand drawer when
you view as a seat (collapsed to its handle on a phone). They sit **above** the HUD, as at the table,
so you can see what a widget would slide under. With it on, each region's box is outlined too.

The toolbar also picks the **preview state**, **view as** (the host, a spectator, or a seat —
private widgets and private seat values appear only for their own seat), which **moments** to draw
(the selected one, all, or none) and the **button state** (below). The selected widget is outlined
in the canvas, and clicking a widget there (anywhere but one of its buttons) selects it; a
selected widget can be resized and nudged right there (see [Drag to resize and
nudge](https://diceytable.com/docs/editor/item-editors/ui-layout#drag-to-resize-and-nudge)). Pressing a
button in the canvas runs no script; the line under the canvas shows exactly what `ui.onAction`
would receive.

**Inspector** (right) — five sub-tabs: Widget, State, Previews, Problems and JSON.

## Widgets

The **Widget** sub-tab edits the selected widget:

- **Widget** — its id (what scripts name), kind, audience (only *all* is public; every other
  audience travels privately to its viewers) and whether spectators may press it.
- **Placement** — region, order within the region, and an optional attachment.
- **Per device** — place the widget differently on one kind of screen: **Desktop**, **Tablet** or
  **Phone** can each take another region or **Hidden**, and an `actions` widget can be folded into
  a **menu button** (one button that opens its buttons). Each setting changes only its own screen,
  so tightening the phone layout never moves anything on a desktop. A prompt or choice is never
  hidden.
- **Text** — the fields the kind uses. Text takes bindings: `{phase}`, `{seat.force}` (the
  viewer's own seat), `{key|fallback}`, plus `**bold**`, `*italic*` and `[color=red]…[/color]`.
  **Insert binding** appends a declared key to the field you choose. Values are always inserted as
  text, never markup.
- **Condition** — show the widget only while rows hold (*is*, *is not*, *is one of*, *is set*,
  *is empty*, *variant is one of*). A condition that nests groups is edited in the JSON sub-tab.
- **Timing** — how long it stays once shown, and what ends it.
- **Theme** — tone, accent (a palette name or `#rrggbb`, contrast-clamped), an icon from the HUD
  icon list, an **icon image** (a game picture drawn in place of the icon), a font from the
  self-hosted list, density, emphasis, and for prompts, choices, results, panels and player panels
  a **header image**.
- **Style** — the widget's own size, place, spacing, colours, border, type and button layout (see
  [Style](https://diceytable.com/docs/editor/item-editors/ui-layout#style)).
- **Kind sections** — buttons, choices, counters and badges (each with an icon and an **icon
  image**, so a game can show its own stat symbols), prompt timing, log length, timer length, a
  panel's basic widgets as JSON, and for a **card spotlight** the card (**Entity id**, **Card id**,
  the required **Card label**), **Show counters**, and its **stat chips** (label, value, **Change**:
  *same* white, *up* green, *down* red, icon and icon image — a chip with an icon or icon image draws
  that symbol instead of its label, which stays its accessible name and tooltip; at most 8). A script usually sets the
  card when it shows the spotlight — see [Card spotlights](https://diceytable.com/docs/scripting/concepts/game-ui#card-spotlights).
- **Search box** (choices) — *Auto* shows a filter box above the options when there are more than
  12, *On* always, *Off* never; **Search placeholder** sets its hint when it is On. The canvas
  preview draws the box (type into it to try the filter); the VR frame draws the letter tabs a
  headset player gets instead. The typed text stays on the player's screen: a pick is still an
  option id.

### Buttons and their states

Every button has five states: **default**, **hover**, **pressed**, **active** (toggled) and
**disabled**. The **Buttons** switcher above the canvas does two things at once: it draws the
selected widget's buttons in that state, and it chooses which state the button's label, icon,
images, tone, accent and emphasis fields edit. Default writes the button itself; every other state
stores only what differs and inherits the rest. Touch has no hover; VR maps hover to the ray or
fingertip.

**Enabled when** and **Active when** bind a state key: a disabled button never sends an action,
and the host refuses one anyway. A press sends data only — the widget id, the action id and, for a
choice, the picked values.

### Style

The **Style** section edits the widget's `style`: a closed list of keys, never CSS. Leave a field
empty to keep the platform's value; the placeholder shows what applies.

- **Applies to** — *All devices* edits the widget's own style. *Desktop*, *Tablet* or *Phone* edits
  that screen's override, which is merged over All devices on that screen only (the placeholders
  then show the All devices values). Pick the matching canvas frame to see it.
- **Size** — width, min width, max width, height and max height, each in **px** or **% of the
  screen**; a width can also be **auto** (fit the content) or **fill** (as wide as the region), a
  height **auto**. A widget is never wider than the screen or taller than 85% of it; content past a
  fixed height scrolls.
- **Position in the region** — **Align** (start, center, end, stretch) and a **Nudge** of up to
  200 px each way.
- **Spacing** — **Padding** and **Margin**, for all sides or per side (above, below, start, end),
  and the **Gap** between the widget's own buttons, chips and rows.
- **Colours and border** — background (a palette name or `#rrggbb`, with a colour well) and its
  opacity, text colour (kept readable on the background: too-close colours are lightened or
  darkened), border width, style and colour, corner radius and shadow.
- **Type** — font size (10-32 px; headings scale with it), weight, text alignment and **Max
  lines** (each paragraph of the body is cut there).
- **Buttons** (actions, prompts and choices) — a row or a column, wrap or one scrolling line, size
  (sm, md, lg), how each button shows its icon and label (beside, under, icon only, label only)
  and where they sit in their row.

**Reset style** clears the style at the chosen scope. A question (prompt or choice) keeps the
sheet's size and place; only its look is styled. VR ignores style.

### Region layout

**Region layout**, under the region map, sets how one region lays out its widgets — the layout's
`regions` map: **Direction** (a column, the platform's, or a row), **Align**, **Justify** (start,
center, end, between), **Gap**, **Wrap**, **Max width**, **Max height** and a **Nudge**. Pick the
region first. **Applies to** works as in Style (and is the same setting): *All devices* edits the
region's own keys, *Desktop*, *Tablet* or *Phone* that screen's keys, merged over them there, with
the All devices values shown as placeholders. When several attached layouts style one region, the
first widget drawn there decides. **Reset region** clears the keys at the chosen scope (at All
devices the per-screen keys stay).

On a **phone**, the compact layout deals widgets into its own corner slots, and a slot takes a
region's style when every widget in it was placed in that one region:

| Phone slot | Takes the style of |
|---|---|
| The stats stack under the dice button | `upper-end` |
| The centre | `center` |
| The action dock (bottom-right) | the region every `actions` widget in it was placed in, when they all share one |
| The card spotlight row | the region every `card` in it was placed in, when they all share one |
| The status pill, the log tab | never: they are one-tap platform controls |

There a region style stays inside the slot's corner: the max sizes can only shrink it, and the
nudge only moves it away from the screen edges it sits against. Preview it in a phone frame.

### Drag to resize and nudge

With a widget selected, the canvas draws **resize handles** on its edges and corners. Drag a handle
to set its **Width** and **Height** in px; drag the widget itself (anywhere but one of its buttons)
to **nudge** it. Both write the Style fields at the current **Applies to** scope (*All devices*
writes `style`, a screen class writes that screen's style), so pick a matching frame to watch it.
While dragging, a readout shows the size or the nudge; hold **Shift** for 8 px steps, press
**Escape** to cancel. A value that reaches its limit (a nudge of ±200 px, the screen's width, 85%
of its height, 16 px at the smallest) stops there and its handles turn amber. Releasing writes the
change once, so one **Undo** reverts the whole drag. With the canvas focused, the **arrow keys**
nudge the selected widget by 1 px (Shift: 8 px).

## Images and fonts

Images come only from **this project's pre-loaded game files**: PNG, JPEG or WebP files the
published manifest declares in `assets`, because every player draws them. The picker lists exactly
those, and the editor and the publish scanner apply the same rule, so an image the editor accepts
always publishes. Files under `media/` (listing art), `originals/`, `rules/` and `thumbnails/`, a
deck's card source images, and files you switched to
[load on demand](https://diceytable.com/docs/editor/interface/topbar#mod-details-the-files-tab) are not offered and are
refused. URLs, `data:`, SVG and paths outside the project are refused as well.

The header image, the theme's icon image and a counter's or badge's icon image also offer each
**string** state key as a binding — `{seat.sideIcon}` for a per-seat key, `{phaseIcon}` for a table
key. A bound image shows whatever path the game's script sets, so one player panel can show every
seat its own faction or side symbol. The value is drawn only when it is itself one of this
project's PNG, JPEG or WebP paths (never a URL), and a private per-seat value only for its own seat.
A fallback image (`{seat.sideIcon|ui/side-neutral.png}`) can be typed in the JSON sub-tab; it is
checked like any other image. Preview fixtures set the values, so the canvas shows the symbols.

Fonts come from a fixed, self-hosted list (`platform`,
`serif`, `display-classic`, `display-bold`, `sci-fi`, `mono`, `retro`, `handwritten`).

## State keys

The **State** sub-tab declares what bindings and scripts read. Each key has a type, a scope (one
value, or **per seat** — read as `{seat.key}`), an optional default, and for per-seat keys a
**private** flag: a private value travels only to its own seat, so an instruction like "play a
card or pass" is not shown to the opponent. A script's `ui.set("howTo", text, seat)` is routed
privately for a key declared private here.

## Preview states

The **Previews** sub-tab holds named fixtures — state values and whose screen to show — such as
*"Red's turn, mid-game, on a phone"*. **Preview this** loads one into the canvas. Fixtures are saved
with the layout for the next author and are never sent to a table.

## Problems

The **Problems** sub-tab lists what the publish scanner would say: errors (an unknown kind, font
or icon; a broken shape; a style or region-layout key that is not in the vocabulary; an image that is not a pre-loaded game file; a layout over 256 KB as JSON;
colliding widget ids across attached layouts; more than 32 layouts in the project or more than 8
attached to one variant) and warnings (a binding that reads an undeclared key or the wrong scope; a condition
naming an unknown variant; buttons no attached script appears to handle). The same layout errors
appear in the editor's status-bar problems list. **Show** selects the widget a problem is about.

## JSON, import and export

The **JSON** sub-tab is the layout as text. Edit it and press **Apply JSON**; the layout's id is
kept. **Export JSON** downloads `<id>.layout.json`, and **Import file…** replaces the layout with
one from disk. A `*.layout.json` opened in the [Plain File editor](https://diceytable.com/docs/editor/item-editors/file)
is checked against the generated layout schema as you type.

## Related

- [Game UI concepts](https://diceytable.com/docs/scripting/concepts/game-ui) — where text belongs: HUD, table text, or
  chat.
- The `ui` scripting API — `ui.show`, `ui.set`, `ui.prompt`, `ui.onAction` — in the
  [Scripting API reference](https://diceytable.com/docs/scripting-api/api).
- [Game variants](https://diceytable.com/docs/mods/manifest/variants) — the scope bar that routes attachments.
