GistUI Lang is what the model writes. It is designed to be cheap in tokens, hard to get wrong, and renderable line by line. You rarely write it yourself, but reading it helps when you debug an answer or write a fixture.
Statements
A program is a list of statements, one per line: id = value.
root = Page(head, body)
head = Header("Q3 revenue", "All regions")
body = Card("Revenue grew **8%**.")rootcomes first. It is what gets drawn.- An id may be used before it is defined. Order:
root, then containers, then leaves, then data tables last. - Ids are short and lowercase. Every id is defined once and used at least once.
Components
Name(positionals, children…, key:value)Loading preview…
root = Card(Header("Storage", "4.2 of 5 GB used", size:sm), Progress(4.2, "Used", max:5, tone:warning), Buttons(Button("Upgrade"), Button("Manage", v:ghost)), gap:md)| Part | Rule | Example |
|---|---|---|
| Positional arguments | Required props, in signature order | Header("Storage", "4.2 of 5 GB used") |
| Children | Any number of components or strings, after the positionals | Card(a, b, c) |
| Named arguments | Optional props, key:value, in any order |
size:sm, max:5 |
Optional props are always named. A skipped argument is never padded with null or "".
Enums and flags are bare words
row = Row(a, b, wrap, gap:lg)
card = Card(a, b, sunk)
tag = Tag("Live", success)gap:lg: an enum value, unquoted.wrap: a flag. The bare word turns a boolean prop on.sunk,success: an enum value alone sets the one prop that has it (v:sunk,tone:success).
The parser decides from the catalog, not from guessing. Two rules keep it unambiguous:
- A statement with that name always wins:
Card(a, row)withrow = …defined is a child. - Inside a list, a bare word is a reference, so strings in lists are quoted:
icons:["user", "users"].
Flag names are reserved words and cannot be ids. The list is at the end of the full reference.
Strings
Strings are JSON strings: double quotes, \n for a new line, no raw line breaks. A string in child position is Markdown.
Loading preview…
root = Card("### Summary", "Revenue is **up 8%**. See the [full report](https://example.com).\n\n- Enterprise grew fastest\n- Churn fell")Strings never interpolate. Join with +: "Total: " + $count.
Tables
Data is written as pipe rows. A line that starts with | continues the table; the first line that does not ends it.
Loading preview…
root = Grid(Card(Chart(rev, type:bar, title:"Revenue")), Card(Table(rev)), cols:2)
rev = |Month|Revenue:n|Target:n
|Jul|380000|350000
|Aug|402000|375000
|Sep|431000|400000- The first row is the header.
Revenue:nmarks a number column,Name:sa text column; without a hint the type comes from the first data row. - The first column is the x axis or category. Every other column is a series.
- One table can feed several components. Here the same
revdraws a chart and a table. - Numeric cells may be written as
$1,200or45%; charts get the number, tables keep the text.
Chart, Table, Stats, Timeline, KeyValue and Pricing all take a table.
Comments, prose and fences
- A line starting with
#is a comment. - A line that is not a statement (a sentence the model added) is ignored, with a warning. It does not break the program.
gistuicode fences are stripped. In inline mode the text outside fences is chat text.#gistui 1on the first line names the format version. It is optional.
What a model gets wrong, and what happens
The format defines how each common slip is read, so every implementation recovers the same way.
| Written | Read as |
|---|---|
'single quotes' |
a string |
key=value |
key:value |
card = Card(a, b at the end of a line |
the bracket is closed |
Card(a, b,) |
the trailing comma is ignored |
{"label": "x"} |
{label: "x"} |
// comment |
a comment |
size:"lg" |
size:lg |
Each is reported as a lenient-syntax warning. See Diagnostics.
Next
- State and expressions:
$state,@query,@each, operators. - Edits and multi-turn: changing a UI that is already shown.
- The full reference: the exact text the model receives.