Skip to content
Language overview

Language overview

kadokit documents are written in KDL v2, a small human-friendly document language. This page shows the overall shape of a document; the following pages go deep on each part.

Anatomy of a document

app { width 480; height 320; title "My App" }

expose "app"
import "styles.ui.kdl"
import "components/Card.ui.kdl"

assets {
    image "bg" source="assets/bg.png"
    font "title" source="fonts/Title.ttf" size=24
}

state {
    count (int) 0
}

style "card" {
    bg-color "#2D2D2D"; bg-opa 255; radius 12
}

component "Badge" {
    prop "text" type="string" default=""
    label { text bind="prop.text"; bg-color "#333"; bg-opa 255; radius 8; padding 4 }
}

script "function double(x) { return x * 2; }"

screen "home" {
    // widgets go here
}
Top-level nodePurpose
appWindow metadata: width, height, title
expose "name"Expose the state store to expressions under this name
import "path.ui.kdl"Import another file (styles, components, state). Cycle-safe
state { ... }Declare reactive state variables
computed "name" expr="..."Derived state value
style "name" { ... }Named style
component "Name" { ... }Reusable component
delegate "Name" { ... }List-view item template (auto-injects item and index)
script "..."Run script code at load time
assets { ... }Declare required images and fonts
screen "name" { ... }Named screen with optional typed parameters and lifecycle hooks

Three ways to set a property

Every widget property can be set three ways, and this is the heart of the language.

Literal: a static value:

slider { min 0; max 100; value 50 }
box { bg-color "#FF0000"; radius 12 }

Expression binding (bind=): one-way reactive. The expression is re-evaluated whenever any state it references changes:

label { text bind="app.count + ' items'" }
box { bg-color bind="app.alarm ? '#FF0000' : '#00FF00'" }

Model binding (model=): two-way reactive. State changes update the widget, and user interaction writes back to state:

slider { value model="app.brightness" }
switch { checked model="app.dark_mode" }

Events and actions

Widgets respond to events with child blocks containing actions:

button {
    label "Click me"
    on-click {
        set "count" expr="app.count + 1"
    }
}

Events cover clicks, presses, value changes, focus, scrolling, keys, rotary encoders, gestures, swipes, and a full drag lifecycle (on-drag-start / on-drag / on-drag-end with release velocity for fling detection). Handlers can read the event payload directly in action expressions: event.x, event.key, event.dx, event.vx.

touch-area {
    on-drag-end {
        set "page" expr="event.vx < -600 || event.dx < -80 ? app.page + 1 : app.page"
    }
}

Actions include set (write state), nav / nav-back (navigation), cmd (invoke firmware commands), eval (script side effects), emit (component signals), array mutations, and widget targeting (show, hide, focus, scroll-to). See the Reference for the full tables.

Widget ids

Any widget can carry an id= for cross-widget references in expressions:

slider id="speedSlider" { value model="app.speed"; min 0; max 100 }
label { text bind="'Speed: ' + id.speedSlider.value" }

Id reads are read-only, non-reactive snapshots, scoped per screen and per component. Forward references within a scope are resolved after the initial build pass.

Control flow

Rendering itself is data-driven:

// Loop over an array state
for "rooms" as="room" {
    label { text bind="room.name" }
    slider { value model="room.brightness" }
}

// Repeat a fixed number of times
repeat count=10 as="cell" {
    box { label { text bind="'' + cell.index" } }
}

// Conditional rendering. Children are created or destroyed reactively
if "app.show_details" {
    label "Details here"
}

// Periodic or one-shot timers
timer {
    interval 1000
    active bind="app.running"
    set "seconds" expr="app.seconds + 1"
}

Comments

// Line comment

/* Block comment */

/- node "commented out node"

The /- form comments out an entire node and its children. This is handy for toggling whole widgets during development.