Getting started
kadokit applications are written as plain text files with the .ui.kdl
extension, in the kadokit DSL. On desktop, the runtime loads a document
directly, validates it, and builds the interface. Edit, rerun, repeat. For
embedded targets there is one extra step: the host compiler produces a
compact .bui binary that the device runtime loads (see
From desktop to device).
Your first interface
app {
width 400; height 300; title "Hello"
}
label "Hello, kadokit!" {
align "center"
}Save this as hello.ui.kdl and run it with the kadokit runtime:
kadokit hello.ui.kdlThis opens a 400×300 window with the text centered on screen. The desktop runtime renders into a window, so iterating on the UI is as fast as editing text.
A real application
A document describes an entire application: metadata, state, screens, and behavior.
// App metadata
app {
width 480; height 320; title "My App"
}
// Expose the state store to expressions as "app"
expose "app"
// Declare reactive state
state {
count (int) 0
}
// Define a screen
screen "home" {
vbox {
fill #true; padding 16; gap 8; align-items "center"
label {
text bind="'Count: ' + app.count"
}
button {
label "Increment"
on-click {
set "count" expr="app.count + 1"
}
}
}
}Three ideas carry most of the language, and the rest of the docs build on them:
- State is declared, not managed. The
stateblock defines typed variables; widgets subscribe to them through bindings. - Bindings replace glue code.
bind=evaluates an expression again whenever the state it references changes;model=also writes user interaction back to state. - Events run actions.
on-click { set ... }mutates state, and the bindings take care of the rest.
The processing pipeline
When a document is loaded:
- Parse: the KDL text is parsed
- Declarations:
import,style,state,component,script,exposeare processed first - Normalize: nodes are converted to a semantic intermediate representation
- Validate: component references and binding expressions are checked, with suggestions for likely names when there are typos
- Build: the tree is compiled to native widgets with reactive subscriptions
- Navigate: the first screen is displayed; other screens are built lazily when first visited
Because validation happens at load time, a typo like bind="app.brighness"
is reported immediately with source location and a suggestion. It is not discovered
as a blank widget in the field.
From desktop to device
The .ui.kdl source files are for development. Deployment to an embedded
target uses a binary compilation step:
kadokit --compile app.ui.kdl -o app.buiThe host compiler parses, validates, and normalizes the document, then emits
a compact .bui binary of the intermediate representation. The device
runtime loads .bui data and builds the widget tree from it directly.
Runtime-only builds contain no DSL parser or compiler, which keeps the
device footprint small and load times fast. CMake helpers integrate the
compile step into a firmware build so .bui assets are regenerated whenever
their sources change.
Desktop and device run the same IR, so what you iterate on is what ships.
Organizing larger apps
A simple app can be a single file. Larger apps split styles and components
across files with import:
// app.ui.kdl
import "styles.ui.kdl"
import "components/Card.ui.kdl"
import "components/Toggle.ui.kdl"
app { width 480; height 320; title "My App" }
// ... state and screensA .ui.kdl file with exactly one widget root and no app block is a
single-file component. It can declare props and slots and be used like
any built-in widget:
// components/Card.ui.kdl
prop "title" type="string" default=""
slot "body"
vbox {
bg-color "#2D2D2D"; bg-opa 255; radius 12; padding 16
label { text bind="prop.title" }
render-slot "body"
}