Skip to content

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 bindings
  • model="...": two-way bindings
  • expr="...": action expressions (set, eval, array-insert, …)
  • if "...": conditional rendering
  • computed "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

ScopeSyntaxWhere
Global stateapp.key (via expose "app")everywhere
Instance stateself.keyinside components
Loop variablesroom.field, indexinside for / repeat / delegates
Propsprop.nameinside component templates and screens with params
Widget refsid.name.propertysame 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.