Skip to content
Introducing kadokit: a declarative UI kit for embedded devices

Introducing kadokit: a declarative UI kit for embedded devices

June 30, 2026

Today we’re publishing kadokit, a UI kit for embedded devices built around a domain-specific language. kadokit is a project of Aptum. This post lays out the design: why we created it, what the language looks like, and the concepts the kit introduces.

Why we created kadokit

kadokit comes out of years of building interfaces for embedded products, and out of three frustrations that kept repeating.

The iteration loop is broken. Embedded UI work means changing C++, recompiling, and flashing hardware to see whether a padding value looks right. A ten-second visual judgment costs minutes. Interface design needs the feedback loop of a text file and an instant preview, not a firmware build.

Users’ expectations moved; embedded tooling didn’t. People spend all day with phone interfaces where motion is physical: things fling, settle, and respond mid-gesture. On microcontrollers, that quality of motion simply didn’t exist. In June 2026 we demonstrated spring-physics interfaces running on a low-cost ESP32-S3 in a few tens of kilobytes of SRAM — to our knowledge a first on microcontroller-class hardware — and declared in a text file rather than hand-coded, a first of its own. kadokit is the kit built around that idea.

UI code has a shape problem. The interface, which is fundamentally a description, gets written as imperative widget construction code, tangled with the logic that updates it. Every label that shows a sensor value needs a callback that finds the label and rewrites its text. Every screen is a function. Every animation is a state machine. The structure of the UI exists only in the programmer’s head, invisible to review, to designers, and to tooling.

So kadokit takes the position that an embedded interface should be a document: a declarative text file that states what the interface is: its widgets, its state, how state maps onto properties, and how interaction maps back onto state. The runtime compiles that document into native widgets at load time and keeps everything in sync from then on. Firmware code talks to the UI through a small, typed bridge instead of touching widgets.

The description language is KDL, chosen because it reads like a config file but nests like a widget tree. UI documents use the .ui.kdl extension.

The core concepts

1. Declared, typed, constrained state

State is a first class declaration, not a variable convention. Each entry has a type, a default, and optionally constraints that the runtime enforces:

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
}

read-only is the interesting one: it makes a value writable only by firmware. Sensor data displayed by the UI can never be corrupted by the UI.

2. Bindings instead of glue code

Any widget property can be bound to an expression that evaluates again when the state it references changes (bind=), and interactive properties can bind two-way, writing user input back to state (model=):

label  { text bind="app.temperature + '°C'" }
box    { bg-color bind="app.alarm ? '#FF0000' : '#00FF00'" }
slider { value model="app.brightness" }

That’s the entire update model. There are no observers to register, no render loops to write, no dirty flags. Expressions are JavaScript, evaluated by an embedded engine designed for constrained devices.

3. Components with typed props, slots, signals, and instance state

Reusable components declare typed inputs (prop), content injection points (slot), outward events (signal), and private state per instance:

component "TodoItem" {
    prop "text" type="string"
    state { done #false }

    hbox {
        checkbox { checked model="self.done" }
        label {
            text bind="prop.text"
            text-color bind="self.done ? '#666' : '#FFF'"
        }
    }
}

Props are reactive: pass value bind="app.temperature" at the call site and the component follows the live state. Property aliases forward a call site model= binding through the component onto an inner widget, so a wrapped slider still two-way binds. A file with a single widget root is itself a component. The file system is the component library.

4. Animation as a property annotation

Motion is configured where the property is bound, not coded somewhere else. Tween easings and spring physics attach to any bound property, and every change, whether from state, firmware, or touch, moves through them:

arc { value bind="app.speed" spring="snappy" }
box { x bind="app.offset"; animate "x" tween-duration=300 tween-easing="out-cubic" }

For coordinated motion across many widgets, motion groups declare staggered animation waves that are triggered by a single play action:

motion-group "charge-wave" {
    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"
}

A 64-cell battery charge animation is one declaration, replayable from a tap or a timer, with zero imperative animation code.

5. Screens as parameterized, lazily built units

Navigation is part of the language: named screens with typed parameters, enter/exit/resume lifecycle hooks, transitions with automatic reversal on back navigation, and a history stack that restores parameters:

screen "room_detail" {
    param "id" type="string"
    transition "slide-left" duration=300
    on-enter { cmd "refresh_room" }

    label { text bind="'Room: ' + prop.id" }
}

// From a list of rooms:
on-click { nav "room_detail" { id bind="item.id" } }

One detail screen serves every room. Screens build lazily on first visit, so a ten screen app starts as fast as a one screen app.

6. A deliberate firmware boundary

The host API is intentionally small: expose state, load a document, read and write values, subscribe to changes, register commands. Data flows down through state; intents flow up through commands (cmd "reboot"). Updates coalesce automatically once per display frame, and explicit batches let firmware update a dozen sensor values with a single UI pass.

7. Develop on desktop, ship a binary

The DSL source is the development format, not the deployment format. On desktop, the runtime loads .ui.kdl files directly and renders into a window. The edit-run loop is as fast as editing text, with no flashing and no cross-compilation. For the device, a host compiler emits the validated intermediate representation as a compact .bui binary:

kadokit --compile app.ui.kdl -o app.bui

The device runtime loads .bui data and builds the widget tree from it directly. Runtime-only builds ship no parser and no compiler. Desktop and device run the same IR, so what you iterate on is what ships.

8. Failing at load time, not in the field

Documents are validated when they load: unknown state variables get suggestions for likely names, type mismatches between bindings and widget properties are flagged, missing required props are errors, and declared assets are checked against what firmware actually registered. A typo is a diagnostic with a source location, not a blank widget discovered after shipping.

What you can build

The concept set composes further than it might sound: a thermostat with constrained state and two screens is a few dozen lines; a smart home panel with room detail screens is a handful of component files; an e-bike dashboard with spring animated gauges, paginated snap scrolling, and custom fonts is one document plus an asset pack. Virtualized list-view handles large datasets by recycling widgets through a delegate template; drag events report release velocity so a card carousel can distinguish a fling from a slow drag in one expression.

The documentation covers every concept in detail, starting from a counter app and ending at motion orchestration.

What’s next

The documentation on this site will grow alongside the runtime, and we’ll write follow-up posts diving into individual design decisions, starting with how the motion engine runs spring physics on a microcontroller. This post stays as published. If you build interfaces for constrained hardware and this resonates, the docs are the place to start, or write to embedded@aptum.fr.