DSL Syntax
TokUI DSL is minimal: brackets wrap one component. The first token is the type, then key:value attributes, trailing text is content.
Basics
[h1 Title] ; self-closing
[card tt:Title] content [/card] ; container, must close with [/type]
[btn tx:"Click me" v:primary clk:onClick] ; space-separated attributes
[tr Alice,25,NYC] ; comma-separated values
ph:"value with spaces" ; quote values with spaces
v:"primary,sm" ; comma-separated variants- Containers must close with
[/type]; self-closing tags hold no children. - Boolean attributes need no value:
req,stripe,dis.
Common attributes
| Short | Meaning | Short | Meaning |
|---|---|---|---|
id | element id (also upd target) | tt | title |
tx | text | l | label |
ph | placeholder | u | url |
s | src / source | n | name |
v | value / variant | act | action |
mtd | method | clk | onclick handler |
sub | onsubmit handler | dis | disabled |
ro/req/chk | readonly/required/checked | w/h/bg/fc | width/height/bg/color |
clk:/sub:handlers must be pre-registered viaTokUI.registerHandler(name, fn)— the server ships no executable code.
Boolean attributes
Just the key, no value:
stripe dis ro req chk multi auto plain round closable bordered open
pill dot leaf inline rounded containerVariants
v:primary generates class tokui-{type}--primary. Combine with commas: v:"primary,sm,pill". Unknown variants are silently dropped (whitelist).
| Component | Allowed variants |
|---|---|
btn | primary danger success warning ghost sm lg pill block |
card | highlight flat bordered center right |
h1~h6 | left center right ribbon underline badge |
p | left center right muted bold sm lg |
Badging a heading:
h3and other headings are self-closing — never nest[h3 text [badge]]. Two official patterns — inline pill viarow v:inline(flex; must usev:inline— the defaultrowis a 12-col grid that squeezes the heading):[row v:inline][h3 SaaS Pro][badge tx:v2.4 pill][/row]; top-right corner badge viabadge-box:[badge-box tx:v2.4][h3 SaaS Pro][/badge-box]. Usetxfor versions/decimals (countruns throughparseIntand truncates2.4to2).badge-boxtext badge usestx(legacylabelstill works).
Dynamic update
[upd] updates an already-rendered component:
[progress id:prog v:0 l:Progress]
[upd id:prog v:50]
[upd id:prog v:100 status:success]
[upd]shines in async incremental updates: the server first sends[progress id:prog v:0], then pushes[upd id:prog v:50]as it advances. Click ⚡ Stream at the bottom-right to replay token by token — the bar jumps 0% → 50% → 100% live.
Component categories
The 150+ components are organized into seven categories, each with full prop tables and editable examples:
| Category | Representative components | Docs |
|---|---|---|
| Basic | headings, buttons, tags, callouts, progress, stats, Markdown, code | basic |
| Form | input, select, switch, slider, rate, date, cascader, transfer, upload | form |
| Layout | card, grid, tabs, collapse, drawer, dialog, timeline, tree | layout |
| Data Display | table, descriptions, pagination, badge, avatar, skeleton, result, empty | data |
| Chart | bar, line, pie, radar, scatter, gantt, funnel (pure SVG, zero deps) | chart |
| AI Chat | bubble, tool-call, thought chain, diff, plan, terminal, sandbox, artifact | ai-chat |
| Showcase | signup form, CRUD, form+table linkage, report-style cards | showcase |
Raw content mode
Inside code, md, diff, terminal, sandbox, and artifact-code containers, [ is treated as a literal character (not parsed as a tag) until the matching [/type]. So brackets inside code snippets never break parsing.
The two modes of paragraph p (leaf vs container)
p is a dual-mode tag: it switches automatically based on whether there is body text inside the tag. Getting it wrong makes child nodes break out as siblings or body text disappear (same trap as card's tx self-closing).
- Leaf mode (body text present):
[p text],[p v:bold text]. The text becomes the paragraph body; it auto-closes when the next block-level sibling arrives (mirrors HTML<p>). The body stream may contain inline children — inline whitelist only:a,tag,b/strong,em,mark,spin,sub/sup,code.
[p First [a u:# register], then run [b npm install].] ✅ inline children sit inside the body- Container mode (no body text, only attrs or empty):
[p]...[/p],[p v:muted]...[/p]. It collects all children until[/p]. When a paragraph must hold block-level components likebtn/form/card/list/table/another[p], use this mode and list children one per line.
[p]
[btn tx:Agree clk:agree]
[btn tx:Decline v:danger clk:reject]
[/p]Common mistake — stuffing a block component into a leaf-mode body stream:
[p Click [btn tx:Submit] to continue.] ❌ btn is not in the inline whitelist; leaf p auto-closes on [btn], which breaks out as a top-level siblingRule of thumb: paragraph holds btn/form/card/list/table or any container → container mode [p]...[/p]; holds a/tag/strong inline or plain text → leaf mode [p text]. Full inline whitelist: src/core/parser.js P_INLINE_CHILDREN.
Body text shaped like word:value (e.g. Q: A:) is parsed as an attribute
For every token after [, the parser checks for an ASCII :: as long as the part before : is an English identifier (Q, A, step, note, id… all qualify), the whole key:value is treated as an attribute. So a "Q&A prefix" like this loses all its body text:
[p Q:How to submit?] ❌ no space: Q becomes the attr name, the whole sentence its value — paragraph body empty, nothing renders
[p Q: How to submit?] ❌ with space: the Q: prefix is eaten as an attribute, only "How to submit?" remainsThis applies to any text component: item, h1~h6, callout's tt, etc. Three fixes:
[p Q:How to submit?] ✅ full-width colon :(parser only matches ASCII :, full-width counts as text)
[p Q How to submit?] ✅ drop the colon, fold the prefix into the body
[p "Q: How to submit?"] ✅ wrap the whole body in double quotes (literal text)For Q&A scenarios prefer the full-width : or no prefix at all — don't write Q:/A: as a Markdown-style prefix into the DSL.
Text containing literal [ ] must be wrapped in double quotes
[ is the tag-start character. When the body text of a normal component (item, p, h1–h6, card title tt, etc.) contains a literal [ or ], it is misread as a nested child tag — the content gets truncated and the tail becomes an orphan node:
[item Math.random() returns [0, 1) float] ❌ [0 parsed as a child tag, item body truncated to "Math.random() returns"Fix: wrap the whole body in double quotes — brackets inside the quotes are kept literal and do not trigger tag parsing:
[item "Math.random() returns [0, 1) float"] ✅
[item "array arr[0] and arr[1]"] ✅
[p "coefficient range [0, 1) half-open"] ✅Note: text inside double quotes is treated as a literal string in full — any
[tag]within is also no longer parsed as a child. If you need both a literal bracket and a real nested child, quote only the bracketed literal and keep the child tag outside the quotes.
Body text without [ ] needs no quoting.
Full reference
- In-site: the seven category docs above — every component has a prop table and a live example.
- Repo: full attribute tables and the container-type list live in
TOKUI_DSL_REFERENCE.md. - Source of truth: containers in
src/core/parser.js(CONTAINERS), boolean attrs inBOOLEAN_ATTRS, variant whitelist insrc/core/renderer.js(VARIANTS).