A UI document is an in-game UI screen authored as data: a .ui.json asset that Shinra cooks to .shinui and the runtime interprets every frame through the same immediate-mode context game code draws with (hi.ui()). It is the data-driven half of user interface: a designer builds a HUD, a pause menu or a nameplate in the editor, a programmer binds numbers into named slots and reads events back by node id, and neither has to touch the other's work.
There is no retained widget tree at runtime. The document is a description; the interpreter walks it and issues the widget calls, so documents get focus navigation, gamepad, scroll physics, modals, world anchors, theming, and the one-draw-call batch for free, and every widget in a document behaves exactly like the same widget built from code.
Authoring is covered in the tutorial; this page is the contract.
Document
{
"kind": "com.hikari.ui", "version": 1, "name": "HUD",
"canvas": [1280, 720], "localization": "game",
"fonts": { "title": "asset://./fonts/poppins/Poppins-Bold" },
"theme": { "accent": [0.85, 0.65, 0.4, 1] },
"slots": { "hp": 72, "callsign": { "type": "text", "default": "Wanderer" }, "alive": true },
"styles": { "hud.caption": { "role": "secondary", "size": "caption" } },
"root": { "id": "root", "kind": "vstack", "width": "fill", "height": "fill", "padding": 16, "children": [
{ "id": "hp_bar", "kind": "progress", "value": { "bind": "hp", "scale": 0.01, "smooth": 10 }, "segments": 5 },
{ "id": "name", "kind": "text", "style": "hud.caption", "text": { "format": "{callsign} · {hp:.0}%" } },
{ "id": "dead", "kind": "text", "text": { "key": "hud_no_link" }, "visible": { "bind": "alive", "not": true } },
{ "id": "resume", "kind": "button", "label": "Resume", "variant": "primary", "anim": "slide_up" }
] }
}| Key | Meaning |
|---|---|
canvas | Design size in UI points, used by the editor stage's device presets. The root normally sizes to the viewport with "width": "fill". |
localization | Domain that {"key": …} text resolves through (assets/localization/<domain>/). Missing keys draw as their name. |
fonts | One asset per FontRole (caption, label, body, title, code). Installed into the world's font slots when the document opens. |
theme | A ThemePatch: any colour the game could pass to ui().setTheme. Applied on first draw through the same snapshot-and-restore path. |
slots | Declared data inputs with preview values. Shorthand "hp": 72 infers the type; the long form declares it. Types: number, boolean, text, color, vec3. |
styles | Named property presets; a node references one with "style" and overrides what it names. |
root | The node tree. |
A node is { "id", "kind", "style", "children", …properties }. Ids are optional but a node the game addresses (an event source, a picked row) needs one; ids are unique across the document and hashed with FNV-1a 64 (hi.ui_types.nodeId). Everything else on a node is a property of its kind.
Node kinds
| Category | Kinds |
|---|---|
| Layout | vstack, hstack, flow, zstack, grid, scroll_area, world_anchor, modal (anywhere in the tree; it is built after the root closes, over everything, while its open binding is true) |
| Collections | repeat (build children once per item), virtual_list (only what is in view) |
| Text | text, rich_text (children are span nodes: text or an icon, each with its own colour, role, scale and font) |
| Controls | button, checkbox, toggle, slider, numeric_field, text_field, dropdown / segmented / tabs (children are option nodes), list_item |
| Display | progress, tooltip (attaches to the widget before it), icon, image, nine_slice, rect, spacer, separator, flex_spacer |
Property names follow the SDK style structs field for field (StackStyle.padding_x is "padding_x"), so the layout and widgets guides describe documents too. Lengths are a number of points, "auto", "fill" or "50%" (fill is what the parent has left on its main axis, so a fixed-width column followed by a fill one ends at the edge; a fill node followed by siblings still takes everything — order the fixed ones first, or size it); colours are [r, g, b, a] or "#rrggbbaa"; choices are the enum names ("justify": "space_between"). Every node also takes visible and the animation properties below.
The authoritative table is src/hikari/src/ui/document/spec.zig. It renders ui_nodes.schema.json at compile time; a Zig test refuses a stale file and Shinra embeds that file for validation and encoding, so adding a property is one row in the spec, and the cooker, the reader, the interpreter and the inspector all follow.
Bindings
A property is a literal, or an object that says where its value comes from:
| Form | Meaning |
|---|---|
{ "bind": "hp" } | Read slot hp. Numbers take "scale", "offset" and "smooth" (a rate in 1/s, eased with ui().animate). |
{ "bind": "hp", "gt": 0 } | Boolean from a comparison: eq, ne, gt, ge, lt, le; "not": true inverts a bool slot. |
{ "format": "{callsign} · {hp:.0}%" } | Text with slot placeholders; :.N sets decimals; {index} is the repeat index. |
{ "key": "hud.health", "args": ["hp"] } | Localized message from the document's domain, with slot values as arguments in the catalog's argument order. |
Inside a repeat or virtual_list, a slot written item.name reads <list>.<index>.name, where list defaults to the node's id — so item 2 of items reads items.2.name, and hud.setItem("items", 2, "name", …) writes it. Structure (lengths, choices, vectors) stays literal, which is what keeps layout cacheable by identity.
Two-way controls (slider, toggle, checkbox, numeric_field, text_field, dropdown, segmented, tabs) write their bound slot back when the player edits them; an unbound control keeps its value in an implicit slot named after the node.
Animation
Every node accepts anim (fade, slide_up, slide_down, slide_left, slide_right) with anim_duration, anim_delay, anim_distance and anim_easing (ease_out, linear, ease_in, ease_in_out, spring), played when the node becomes visible; and anim_loop (pulse, blink, bob) with anim_rate. Containers animate their subtree through the stack's group opacity and offset; a leaf is wrapped in one. Smooth bindings ("smooth") cover value animation, and ui().animate remains available to code.
Runtime
hi.ui().openDocument(AssetRef) opens a document game code owns; a scene actor with a ui component ("ui": { "document": "asset://./ui/hud", "order": 0, "opacity": 1, "interactive": true }) opens one the scene owns and draws it every frame after entity updates. Both return a UiDocument:
const hud = hi.ui().documentOf(hi.world().find("hud")) orelse return;
hud.setNumber("hp", 72);
hud.setText("callsign", "Wanderer");
hud.setItem("items", 0, "name", .{ .text = "Flare" });
var event: hi.ui_types.UiDocumentEvent = .{};
while (hud.pollEvent(&event)) {
if (event.node_hash == hi.ui_types.nodeId("resume") and event.kind == .clicked) resume();
}draw builds a code-owned document into the current frame; state reports pending while the cook or bundle is still arriving. Events are clicked, changed (while dragging or typing) and committed, carrying the node id, its hash, the value and the repeat item index; unpolled events are dropped on the next draw. Slots survive a hot reload — a designer moving a bar keeps the number the game wrote into it — and instances die with their World, so Play/Stop needs no cleanup. The document's theme block is a patch, not a theme: on first draw it overrides just the listed tokens over whatever theme is in force, so a game that installed its own palette with ui().setTheme sees the document inherit everything it did not name, and the backup is restored when the last document closes. Sprites go through the frame's one imagery slot like ui().image — a document and the game's own UI must draw from the same atlas in a frame, which is why the example session card steps aside for the inventory scene; fonts and the theme install on open.
Samples in the example game: assets/ui/hud.ui.json (scenes/ui_document_demo) is a HUD — bars, a repeat list, a few controls; assets/ui/inventory.ui.json (scenes/rpg_inventory, driven by src/entities/rpg_inventory_entity.zig) is a full screen — two nine-slice panels from the ui/aurora atlas, tabs and a segmented sort that the game reads back, a 6-column sprite grid built by repeat inside grid with a list_item plate under each cell for hover, selection and the click event, a virtual_list of quests, a details column whose every field is a slot, a confirm modal bound to a slot, localized strings with a slot argument, and spring enter animations. I toggles it.
A tool window's imagery is graphics/renderer/render_frame.zig UiImagery: a CPU atlas the renderer uploads and caches by id (asset thumbnails), or a snapshot of a texture already resident (a sprite sheet). Every surface carries one or the other through the mailbox, and renderer_pipelines.bindUiImagery selects it at the UI pass.
Files: ui/document/{spec,format,runtime}.zig (vocabulary, UID1 layout and reader, interpreter; Shinra is the only compiler — the editor stage asks it through the pipeline daemon), scene/world/ui/documents.zig (per-World instance table, hot reload, host callbacks), scene/components/ui_document*.zig (the ui component), Shinra formats/internal/shinui.rs (cook).
Editor
Double-click a .ui.json (or Create → New UI Document) for the authoring window: one toolbar (undo, redo, device preset, reset preview, save) over the outline, the stage and the properties rail side by side, with slots and the console below. The outline on the left lists the tree with the scene hierarchy's guide lines; Add child picks a kind and appends it under the selection (or beside a leaf), with Up, Down and Delete for the rest. The stage in the middle runs the real interpreter on the document as it is edited: controls work, animations play, and the pointer picks the node under it; a device preset fixes the viewport to a phone or a TV instead of the pane. Sprites on the stage are the real thing: the stage retains the referenced atlas's texture through the asset store and the window publishes that resident texture as its imagery (UiImagery.texture, the same path the game's sprite binding takes), so a 2048-pixel sheet is sampled at full resolution rather than through the 128-pixel browser thumbnail; an atlas that has not been cooked yet shows a console warning instead of its sprites and is retried when the cook lands. The outline, the stage and the rail are separated by draggable seams (so are the slots and the console below), sized in points until dragged and by fraction after, so a split survives a window resize. The stage applies the document's theme patch over the engine default theme — a palette the game installs from code is not knowable from its assets, so the stage shows the patch, not the game's base. The properties rail on the right is generated from the spec table — grouped as content, layout, appearance and animation — with a Slot field on every bindable property, Smooth on numbers, and a Message key on text. The slots panel below declares slots with the preview values the stage reads; the console lists validation errors and jumps to the offending node. Every edit is undoable through the shared history; a quiet moment writes the file and Shinra recooks it through the normal pipeline, so a placed HUD in the viewport follows the edit.