Skip to content

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

NodeDescription
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

FormSyntaxBehavior
Literalvalue 50Static
One way bindingvalue bind="app.x * 2"Evaluates again when referenced state changes
Two way bindingvalue 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

EventTriggerPayload fields
on-clickWidget clicked/tappedx, y, local_x, local_y
on-short-clickShort press (not long-press, not scroll)position
on-single-clickSingle click (not double)position
on-double-clickDouble-tapposition
on-changeValue changed (slider, text, …)type, code
on-press / on-releaseTouch down / upposition
on-press-lostPointer left widget during pressposition
on-long-pressHeld for 400 msposition
on-long-press-repeatRepeats while heldposition
on-pressingContinuous while pressedposition
on-focus / on-defocusFocus gained / losttype, code
on-scroll / -begin / -endScroll position changedscroll_x, scroll_y
on-keyKeyboard input (auto-focusable)key, key_code
on-rotaryRotary encoderdiff
on-gestureRaw gesture, any directiondir
on-swipe-left/right/up/downDirectional swipetype, code
on-drag-startDrag threshold crossed (5 px)x, y, dx, dy, delta_x, delta_y
on-dragContinuous drag movementsame as above
on-drag-endDrag releasedabove + vx, vy, cancelled
on-mountWidget built and mountednone

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

ActionSyntaxEffect
setset "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
navnav "screen" [transition= duration=] { params }Navigate (pushes history)
nav-backnav-backPop history
cmdcmd "name"Invoke a firmware-registered command
evaleval "js"Evaluate script for side effects
emitemit "signal-name"Fire a component signal
playplay "motion-group"Trigger a motion group
array-insertarray-insert "key" [index=] expr=/value=Insert into array state
array-removearray-remove "key" expr=/index=Remove from array state
array-movearray-move "key" from= to=Reorder array element
focusfocus "id"Focus a widget
scroll-toscroll-to "id"Scroll a widget into view
scroll-to-x/yscroll-to-x "id" x= anim=Scroll to absolute offset
scroll-byscroll-by "id" x= y= anim=Scroll by relative offset (clamped)
update-snapupdate-snap "id" anim=Re-snap to nearest snap point
show / hideshow "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)"