Components
Components are reusable UI building blocks with typed props, content slots, outward signals, and optional state per instance.
Defining and using a component
component "Card" {
prop "title" type="string" default=""
slot "body"
vbox {
bg-color "#2D2D2D"; bg-opa 255; radius 12; padding 16; gap 8
label { text bind="prop.title"; text-color "#888888" }
render-slot "body"
}
}
Card {
title "Sensors"
body {
label "Temperature: 22°C"
label "Humidity: 65%"
}
}Props
Props are the component’s typed inputs:
component "StatCard" {
prop "title" type="string" required=#true
prop "value" type="int" default=0
prop "color" type="color" default="#FFFFFF"
// ...
}The shorthand annotation form also works: prop "idx" (int) 0.
Props without a default are required. The validator reports an error
if one is missing at the call site, at load time.
Reactive prop access
Inside a component template, prop.name gives typed, reactive access in any
expression context:
component "Gauge" {
prop "value" type="int" default=0
arc { value bind="prop.value" }
}
// Forward a live state binding. The gauge follows app.temperature
Gauge { value bind="app.temperature" }When the caller passes bind=, the prop updates automatically as that state
changes. Components compose reactively without any wiring.
Slots
Slots let callers inject entire widget subtrees:
component "Section" {
prop "title" type="string" default="Section"
slot "content"
slot "footer"
vbox {
label { text bind="prop.title" }
render-slot "content"
render-slot "footer"
}
}
Section {
title "Devices"
content {
button "Light"
button "Lock"
}
footer {
label "Last updated: 5m ago"
}
}Slot content evaluates in the caller’s scope. Bindings inside a slot reference the caller’s state, not the component’s.
Property aliases
Aliases forward a call site binding directly to an inner widget, enabling
two-way model= bindings through components:
component "SettingSlider" {
prop "title" type="string" default="Setting"
prop "value" type="alias" target="sl.value"
hbox {
label { text bind="prop.title" }
slider id="sl" { min 0; max 100 }
}
}
SettingSlider {
title "Brightness"
value model="app.brightness" // forwarded to the inner slider
}Signals
Signals let components communicate events outward:
component "DeviceRow" {
prop "name" type="string"
signal "on-toggle"
hbox {
label { text bind="prop.name" }
switch {
on-change { emit "on-toggle" }
}
}
}
DeviceRow {
name "Lights"
on-toggle { set "lights" expr="!app.lights" }
}Instance state
Components can hold state that is independent per instantiation, referenced
with self.:
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'"
}
}
}Each TodoItem tracks its own done, with no key management or external
bookkeeping.
Single-file components
A .ui.kdl file with a single widget root and no app block is automatically
a component. It supports prop, slot, signal, state, aliases, and its
own import for shared styles:
// components/Card.ui.kdl
prop "title" type="string" default=""
slot "body"
import "../styles.ui.kdl"
vbox {
style "card"
label { text bind="prop.title"; style "subtitle" }
render-slot "body"
}Single-file components can also be rendered standalone for preview and testing, using their declared prop defaults.
Delegates
Delegates are components designed for virtualized lists. They auto-receive
item and index props:
delegate "ItemCard" {
hbox {
label { text bind="item.name" }
label { text bind="'' + index" }
}
}
list-view "rooms" as="item" delegate="ItemCard" {
item-height 64
}