Dicey Table

TableUiWidgetType

The eight values a table UI element's type can take. A mod creates and updates elements with api.setUiElement; the type decides what renders and which props are read.

8 values, declared as tableUiWidgetTypeSchema in packages/shared/src/tableObjects.ts.

Only four of them dispatch an interaction. That is the fact worth taking away from this page, and it is the reason a hook on a panel never fires.

The values#

Value What it renders Props it reads Interaction it dispatches
button A pressable control. text, disabled, variant, selected, onClick, hook "click" — plus the custom hook named by onClick, falling back to hook
checkbox A labeled tick box. text, checked, onChange, hook "change", with checked in the payload — plus the custom hook named by onChange, falling back to hook
input A single-line text field. value, placeholder, onChange, hook "change", with value in the payload — plus the custom hook named by onChange, falling back to hook
select A drop-down built from options. value, placeholder, disabled, options, onChange, hook "change", with the chosen value in the payload — plus the custom hook named by onChange, falling back to hook
text A static label. text, variant, hook none
panel A container other elements nest inside. hook, onDismiss none by itself — onDismiss fires from a mode: "modal" close button
canvas A drawing surface. hook none
layout A container that arranges its children. hook none

Every type accepts id, parentId, order, ownerSeat, visibility, presentation, layout, metadata and arbitrary extra props. ownerModId is set by the host and cannot be supplied.

layout takes TableUiLayoutHints — direction, alignment, wrap, grow, scroll, gap, padding and maxHeight — which is how a container arranges its own children. variant on a text or a button names a presentational ROLE from a closed list; there is no colour, font or size prop on any widget.

The dead hook prop#

Known gap. Only button, checkbox, input and select read an interaction hook from a widget's props — a button from onClick falling back to hook, the other three from onChange falling back to hook (apps/web/src/ui/App.tsx, the mod UI element renderer). text, panel, canvas and layout accept the prop, store it, replicate it and never dispatch anything, because none of them has an interaction to dispatch from. Every widget type renders and nests correctly, and the four interactive types fire reliably. Attach your hook to the button, checkbox, input or select inside the container rather than to the container.

The one exception is onDismiss: a container drawn with presentation.mode: "modal" gets a close button that dispatches it. That is a dialog affordance the chrome supplies, not the container becoming clickable. See Known limitations.

An unrecognized type becomes panel#

setUiElement sanitizes type: any value outside the eight — a typo, a number, undefined on a create — is replaced with panel, silently, with no diagnostic. A misspelled "buton" therefore produces an empty container that renders nothing and dispatches nothing. On an update, an absent type keeps the element's existing type instead.

Limits worth knowing before you build a tree#

  • 2000 elements across the whole table. Past that, setUiElement resolves null for a create and writes a warning to the mod console.
  • 120 UI mutations per tick, counted across all mods together. Past that, setUiElement resolves null and warns. Build a panel in one pass rather than one element per hook.
  • A parentId that names no existing element throws UI parent element not found: <id>. An element naming itself as its own ancestor is quietly reset to a root element.

The top-row anchors need their alias in a mod script#

presentation: { mode: "screen", anchor } stores one of nine anchors and accepts twelve spellings. The three top-row anchors have a second, mod-safe spelling, and in a mod script it is the only one that works:

Write this in a mod script It is stored, and read back, as
"upper-left" "top-left"
"upper-center" "top-center"
"upper-right" "top-right"

The other six — middle-left, middle-center, middle-right, bottom-left, bottom-center, bottom-right — have one spelling each and are written exactly as they are stored.

Anchors are placed inside the table's own chrome#

A screen anchor is resolved against the playable area, not the raw browser viewport. In particular the layer stops short of the bottom of the screen by however much the seated player's hand drawer occupies, so a bottom-left / bottom-center / bottom-right widget sits above the player's cards rather than behind them.

This is the platform's job and not yours: a mod is never told whether the viewer is seated, so it has no way to work out the right offset — and a fixed one guessed for a desktop layout is wrong on a phone. Anchor to the edge you mean and let the platform keep you clear of it. A negative offsetY on a bottom-* anchor still pushes the widget down toward (and past) that edge, so use one only when you deliberately want the widget to overlap the drawer.

By design. The static scanner runs five whole-word regular expressions over raw script text with no lexing (packages/shared/src/modManifest.ts, bannedScriptPatterns). top is one of its dom-access tokens, and because " and - are both non-word characters the word boundary fires inside the string too — so a script containing the literal "top-left" is rejected before it can be published. Matching inside strings is the point of the rule (it is what catches self["top"]), so the regex is a security boundary and was not loosened. The aliases were added to the schema instead (packages/shared/src/tableObjects.ts, TABLE_UI_SCREEN_ANCHOR_ALIASES).

The aliases are input-only. Every parse site normalizes an upper-* value to its canonical top-* before anything is stored, so replicated state, snapshots, getUiState() and listUiElements() only ever carry one spelling per anchor. A mod that writes "upper-right" and then compares a read-back anchor against "upper-right" will never match — compare against the canonical value, which you can name without tripping the scanner by building it ("t" + "op-right") or, better, by keeping your own flag.

Nothing outside a mod script needs the alias: the editor, setup files and imported snapshots may use either spelling and are normalized the same way. Per-rule workarounds are on Script safety.

See also#