Language
kadokit uses KDL v2 as its description language. This page lists the public nodes and syntax documented on this site; the Concepts pages explain how to use them.
Top-level nodes
| Node | Description |
|---|---|
app { width; height; title } | App window metadata |
expose "name" | Expose the state store to expressions under this name |
import "path.ui.kdl" | Import another file. Paths relative to the importing file; cycle-safe; depth limit 8 |
state { ... } | Declare reactive state variables |
computed "name" expr="..." | Derived state value |
style "name" [state="..."] { ... } | Named style, optionally a state variant |
component "Name" { ... } | Reusable component |
delegate "Name" { ... } | List-view item template. It receives item and index automatically |
script "js code" | Run JavaScript at load time |
assets { image ...; font ... } | Asset manifest for validation |
screen "name" { ... } | Named screen |
State declarations
state {
brightness type="int" default=70 min=0 max=100
mode type="string" default="heat" enum="heat,cool,auto"
temperature type="int" default=22 read-only=#true
fan (bool) #false // shorthand: (type) default
rooms type="array" {
- { name "Living Room"; brightness 70 }
}
}Types: int, string, bool, color, array. Constraints: min/max
(clamped), enum (validated), read-only (firmware-writable only).
Property value forms
| Form | Syntax | Behavior |
|---|---|---|
| Literal | value 50 | Static |
| One way binding | value bind="app.x * 2" | Evaluates again when referenced state changes |
| Two way binding | value model="app.x" | Also writes user interaction back to state |
model= is valid only on: slider/arc/bar value, switch/checkbox
checked, dropdown/roller selected, and textarea text. It must use
a plain state reference, not an expression.
Widget ids
slider id="speedSlider" { value model="app.speed" }
label { text bind="'Speed: ' + id.speedSlider.value" }Readable properties: x, y, width, height, hidden, value,
checked, selected, text. Id reads are read-only, non-reactive
snapshots, scoped per screen/component; forward references in the same scope
resolve after the initial build. Missing ids return undefined silently.
Events
| Event | Trigger | Payload fields |
|---|---|---|
on-click | Widget clicked/tapped | x, y, local_x, local_y |
on-short-click | Short press (not long-press, not scroll) | position |
on-single-click | Single click (not double) | position |
on-double-click | Double-tap | position |
on-change | Value changed (slider, text, …) | type, code |
on-press / on-release | Touch down / up | position |
on-press-lost | Pointer left widget during press | position |
on-long-press | Held for 400 ms | position |
on-long-press-repeat | Repeats while held | position |
on-pressing | Continuous while pressed | position |
on-focus / on-defocus | Focus gained / lost | type, code |
on-scroll / -begin / -end | Scroll position changed | scroll_x, scroll_y |
on-key | Keyboard input (auto-focusable) | key, key_code |
on-rotary | Rotary encoder | diff |
on-gesture | Raw gesture, any direction | dir |
on-swipe-left/right/up/down | Directional swipe | type, code |
on-drag-start | Drag threshold crossed (5 px) | x, y, dx, dy, delta_x, delta_y |
on-drag | Continuous drag movement | same as above |
on-drag-end | Drag released | above + vx, vy, cancelled |
on-mount | Widget built and mounted | none |
All events include event.type (string) and event.code (int). Drag
payloads: dx/dy are total displacement, delta_x/delta_y per-frame
movement, vx/vy release velocity in px/s. For fling detection, a short fast
flick has small dx but large vx). Drags have a 5 px dead zone, and click
events are suppressed once a drag starts.
event.* is only valid inside event handler actions. Using it in bind=,
model=, for sources, or if conditions produces a validator warning.
Actions
| Action | Syntax | Effect |
|---|---|---|
set | set "key" expr="..." or value=... | Write state. expr="$self" as the whole expression reads the triggering widget’s value; in larger expressions use widget.value / widget.checked / widget.selected / widget.text |
nav | nav "screen" [transition= duration=] { params } | Navigate (pushes history) |
nav-back | nav-back | Pop history |
cmd | cmd "name" | Invoke a firmware-registered command |
eval | eval "js" | Evaluate script for side effects |
emit | emit "signal-name" | Fire a component signal |
play | play "motion-group" | Trigger a motion group |
array-insert | array-insert "key" [index=] expr=/value= | Insert into array state |
array-remove | array-remove "key" expr=/index= | Remove from array state |
array-move | array-move "key" from= to= | Reorder array element |
focus | focus "id" | Focus a widget |
scroll-to | scroll-to "id" | Scroll a widget into view |
scroll-to-x/y | scroll-to-x "id" x= anim= | Scroll to absolute offset |
scroll-by | scroll-by "id" x= y= anim= | Scroll by relative offset (clamped) |
update-snap | update-snap "id" anim= | Re-snap to nearest snap point |
show / hide | show "id" | Toggle visibility |
Multiple actions in one event block execute sequentially.
Control flow
// Loop over array state. Loop var fields are bindable, `index` is available
for "rooms" as="room" {
label { text bind="room.name" }
}
// Fixed-count repetition
repeat count=10 as="cell" {
label { text bind="'' + cell.index" }
}
// Conditional rendering. Children are created or destroyed reactively
if "app.show_details" {
label "Details here"
}
// Timers: repeat by default, one-shot with repeat #false, reactive with active bind=
timer {
interval 1000
active bind="app.running"
set "seconds" expr="app.seconds + 1"
}
// Virtualized list
list-view "items" as="item" delegate="ItemCard" {
item-height 64; buffer 3
}Screens
screen "room_detail" {
param "id" type="string" // required (no default)
param "mode" type="string" default="view"
transition "slide-left" duration=300
bg-color "#1A1A2E"
on-enter { ... } // every activation
on-exit { ... } // navigating away
on-resume { ... } // reused without rebuild
// widgets...
}Access params with prop.name. Transitions: none, fade, slide-left,
slide-right, slide-up, slide-down (auto-reversed on back navigation).
Components
component "Name" {
prop "title" type="string" default="" // no default → required
prop "value" type="alias" target="sl.value" // forwards bind=/model= to inner widget
slot "body" // caller-injected subtree
signal "on-confirm" // outward event
state { expanded #false } // state per instance (self.*)
vbox {
label { text bind="prop.title" }
render-slot "body"
}
}Prop types: string, int, bool, color, array; untyped props pass
through as strings. Slot content evaluates in the caller’s scope. A
.ui.kdl file with one widget root and no app block is a single-file
component.
Assets
assets {
image "bg" source="assets/bg.png"
font "title" source="fonts/Title.ttf" size=32
}Declared names are validated at compile time (literal src, text-font,
list-text-font, symbol references) and at load time (against the
firmware-registered registries). Load continues on missing assets. Blank
images, fallback fonts.
Motion groups
motion-group "name" {
target "children"
stagger grid="16x4" row-offset=90 col-offset=90 from="first" max-active=18
to "transform-scale" 256 from=64 tween-duration=440 tween-easing="out-cubic"
}Triggered with play "name" from any event, on-mount, or timer. See
Animations → Motion groups.
Comments
// Line comment
/* Block comment */
/- node "commented out node (with children)"