Skip to content
Getting started

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.kdl

This 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:

  1. State is declared, not managed. The state block defines typed variables; widgets subscribe to them through bindings.
  2. Bindings replace glue code. bind= evaluates an expression again whenever the state it references changes; model= also writes user interaction back to state.
  3. Events run actions. on-click { set ... } mutates state, and the bindings take care of the rest.

The processing pipeline

When a document is loaded:

  1. Parse: the KDL text is parsed
  2. Declarations: import, style, state, component, script, expose are processed first
  3. Normalize: nodes are converted to a semantic intermediate representation
  4. Validate: component references and binding expressions are checked, with suggestions for likely names when there are typos
  5. Build: the tree is compiled to native widgets with reactive subscriptions
  6. 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.bui

The 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 screens

A .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"
}

Next steps