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,inputandselectread an interaction hook from a widget's props — abuttonfromonClickfalling back tohook, the other three fromonChangefalling back tohook(apps/web/src/ui/App.tsx, the mod UI element renderer).text,panel,canvasandlayoutaccept 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 thebutton,checkbox,inputorselectinside the container rather than to the container.The one exception is
onDismiss: a container drawn withpresentation.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,
setUiElementresolvesnullfor a create and writes a warning to the mod console. - 120 UI mutations per tick, counted across all mods together. Past that,
setUiElementresolvesnulland warns. Build a panel in one pass rather than one element per hook. - A
parentIdthat names no existing element throwsUI 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).topis one of itsdom-accesstokens, 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 catchesself["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 canonicaltop-*before anything is stored, so replicated state, snapshots,getUiState()andlistUiElements()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#
api.setUiElement— creating and updating an element.api.deleteUiElement— removing one.onUiEvent— the payload an interaction sends.- Mod hooks and capabilities — the custom hook-name channel.
- Script safety — the five patterns and their workarounds.
