Introducing kadokit: a declarative UI kit for embedded devices
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.buiThe 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.