Scripting
kadokit embeds a lightweight JavaScript engine for expressions, computed values, and helper functions. Scripts stay small by design. The UI structure is declarative; script fills in formatting and logic.
Script blocks
Define functions and initialize state at load time:
script """
function formatTime(s) {
var m = Math.floor(s / 60);
var sec = s % 60;
return (m < 10 ? '0' : '') + m + ':' + (sec < 10 ? '0' : '') + sec;
}
"""Raw strings skip escape processing:
script #"""
app.set('items', ['Buy milk', 'Walk the dog', 'Write tests'])
"""#Where expressions run
JavaScript expressions appear in five places:
bind="...": reactive property bindingsmodel="...": two-way bindingsexpr="...": action expressions (set,eval,array-insert, …)if "...": conditional renderingcomputed "name" expr="...": derived state
Available in expressions: arithmetic, string methods, comparisons, ternaries,
boolean logic, Math builtins, and your own functions:
bind="app.speed * 3.6"
bind="Math.min(app.value, 100)"
bind="app.name.length > 10 ? app.name.substring(0, 10) + '...' : app.name"
bind="statusText(app.temperature, app.humidity)"For reactive bindings, pass state values into helper functions as arguments
(formatTime(app.seconds)). State reads hidden inside function bodies are
evaluated but not tracked as reactive dependencies.
Bridge functions
Available in all expressions and script blocks:
// C-style formatting: %d, %s, %%
label { text bind="fmt('%d°C', app.temperature)" }
// State access
script "app.set('count', 42)"
on-click { eval "app.set('brightness', 100)" }
// Navigation
on-click { eval "nav('settings')" }
on-click { eval "nav.back()" }
// Firmware commands
on-click { eval "cmd('shuffle')" }
// Component instance state
on-click { eval "self.set('count', self.count + 1)" }The triggering widget’s value
In a set action, $self is a shorthand that reads the triggering widget’s
current interactive value when used as the entire expression:
slider {
min 0; max 100
on-change { set "brightness" expr="$self" }
}Inside larger expressions (in set or eval), the widget’s value is
available as the widget object instead:
slider {
on-change { set "brightness" expr="Math.min(widget.value, 90)" }
}
switch {
on-change { eval "app.set('mode', widget.checked ? 'night' : 'day')" }
}The value is typed per widget: widget.value for integer controls such as
slider, arc, and bar; widget.checked for switch and checkbox;
widget.selected for dropdown and roller; widget.text for textarea.
Variable scopes
| Scope | Syntax | Where |
|---|---|---|
| Global state | app.key (via expose "app") | everywhere |
| Instance state | self.key | inside components |
| Loop variables | room.field, index | inside for / repeat / delegates |
| Props | prop.name | inside component templates and screens with params |
| Widget refs | id.name.property | same screen/component scope; read-only snapshot |
Scope of the engine
The engine is deliberately small. It is there for expressions and helpers, not application logic:
- Language: a JavaScript subset with no
async/await, no modules, no DOM - Contained: runaway scripts (like infinite loops) are detected and terminated without taking the UI down
- Clean lifecycle: the scripting context is fully reset on reload, so nothing leaks between loads
Heavier logic belongs in firmware, exposed to the UI through state and commands.