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 node | Purpose |
|---|---|
app | Window 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.