Document pages come in two forms producing the same result: .dhml pages are markdown prose whose kdl fences hold element islands, and .kdl pages are one declarative element tree under a top-level page node.
file:book() loads a single page and dir:book() loads a directory whose book.kdl lists the title and chapters. A script-free book ships as a content_type "book" asset pack any app can open; a book that opens itself is a kind "app" package whose init.lua mounts its own bundle with pkg:dir():book():mount{}. Give an element an id (every element accepts one) and the hosting app reaches its live handle via book:element; markdown headings are addressable the same way through their slug.
# Solar output
Daily production, with the *peak* day **highlighted**.

```kdlchartid="solar"height=300 {
linecolor="#e67e80"width=2 {
pointx=0y=12.5pointx=1y=14.1
}
}
```
The same chart as a listing, shown but not run:
```kdl source
chart id="solar" height=300 { }
```
A button you can read and click:
```kdl echo
button id="refresh" "Refresh"
```
Those two forms can be the chapters of a book. A book.kdl at the book's root names the title and lists the chapters in reading order; each file is a page (.dhml or .kdl) relative to the book directory:
Prose and string/number properties may carry {{ expression }} templates: Lua expressions evaluated against the hosting app's globals on every render (mount, go, refresh). A property that is exactly one template keeps the value's own type, so height="{{ h }}" can stand in for a number; an unclosed {{ is left as literal text. A page loaded with bytes:book() runs templates only when its options turn templates on; otherwise its prose shows {{ }} as written, and an if, a for or a property that holds a template raises at load. A lua fence — or a script node, which may load an external script file="..." — runs once at load, and a kdl fence is an element island; an optional mode word makes either fence a listing instead — source shows the code without running it, and echo shows the listing and keeps the block live. Inline code and every other fenced language stay verbatim. The if / for nodes render conditionally and per-item; include splices another page in at load, and a markdown image () embeds an asset resolved against the book directory, so it needs a directory-loaded book.
For example, this page sets up its data in a load-time lua fence, then renders it: greeting the current user, doubling a running total, repeating a row per series item, and gating a detail chart on a flag:
dhml
# Live readings```lua
user = { name = "Ada" }
total = 21
base = 10
series = { { name = "edge", value = 3 }, { name = "core", value = 7 } }
show_detail = true
refresh = function() end``````kdlcolumngap=8 {
label"Hello, {{ user.name }}"label"Total: {{ total * 2 }} kWh"font_size="{{ base + 6 }}"button"Refresh"on_click="{{ refresh }}"forvar="s"in="series" {
label"{{ s.name }}: {{ s.value }}"
}
ifcond="show_detail" {
chartid="detail"height=160 {
linecolor="#7fbbb3"width=3 {
pointx=0y=3pointx=1y=7pointx=2y=5
}
}
}
}
```
Layout and style properties are the ui.NodeStyle fields, restricted to the shapes a KDL scalar can carry; color-valued properties take hex literals.
Elements 41
button
DHML
A button; the positional argument is the label. Give it an id and bind on_click from the hosting app via book:element.
the button is a card, whose content is the elements created under it
Button label — a plain string, or a datumhue.i18n.t(...) message that follows the active locale.
accessible_label
string | Message
the label's text names the button
The name assistive technology announces, in place of the label's text. Required without text. A datumhue.i18n.t(...) message follows the active locale.
focusable
boolean
#true
Whether the button takes focus: a navigation stop with a focus ring. false leaves it activated by pointer and touch only; it keeps its accessible name.
enabled
boolean
#true
Whether the button starts enabled. A disabled button is grayed out and ignores clicks and keyboard activation until elem.enabled = true.
keep_input_focus
boolean
#false
Whether pressing the button leaves a focused text input of your app focused, keeping its caret, its selection and a phone's on-screen keyboard. Otherwise the press takes focus from the input, as a press anywhere outside it does.
on_click
{{ expression }}
–no default
Click callback.
on_hover
{{ expression }}
–no default
Called when a pointer (the mouse or a touch) enters the button or any of its descendants.
on_hover_exit
{{ expression }}
–no default
Called when the last pointer leaves the button and all of its descendants; a press held inside it keeps it entered until the press ends.
on_press
{{ expression }}
–no default
Called when a pointer (the mouse or a touch) presses the button with its primary button.
on_release
{{ expression }}
–no default
Called when that press ends: released anywhere, or taken by a drag, a drag scroll or a long press.
on_double_click
{{ expression }}
–no default
Called on a double-click: two presses on the button within the double-click window.
Category column splitting rows into one series per distinct value (data sources only). Each series draws as its own area, takes a color from color_scale or the category palette in first-seen order, and contributes its own legend item.
color_scale
{{ expression }}
the category palette
Category color scale assigning per-series colors; requires series_column.
readout
"interpolate" | "nearest"
"interpolate"
Crosshair readout interpolation mode.
color
"#rrggbb"
the theme's chart-label color at half opacity, restyled on a theme switch
Fill color.
points
{{ expression }}
the data or view source
Inline points, an array of vec2s.
data
{{ expression }}
the points or view source
Bind to a tabular data source.
view
{{ expression }}
the points or data source
Bind to a streaming data view.
x_column
string
the call raises with data
X column name of a data source.
y_column
string
the call raises with data
Y column name of a data source.
x_scale
{{ expression }}
the area's x scale
X scale.
y_scale
{{ expression }}
the area's y scale
Y scale.
name
string
no name; automatic legends skip the mark
Legend name.
max_points
number
every point is kept
Most points an inline-points mark keeps: past it, each Mark:push_point drops the oldest.
Pre-computed summaries (low/high are the whisker ends). Exactly one of entries or data.
data
{{ expression }}
the entries source
Tabular source; requires category_column and value_column. Summaries take quartiles by linear interpolation, whiskers at the farthest values inside the 1.5 IQR fences, and the rest as outliers.
category_column
string
the call raises with data
Category column to group by (data sources).
value_column
string
the call raises with data
Numeric column to summarize (data sources).
box_width
number
0.6
Box width as a 0..1 fraction of the band slot.
orientation
"vertical" | "horizontal"
"vertical"
Box orientation. The category axis is x when vertical, y when horizontal.
color
"#rrggbb"
the theme's chart-label color, restyled on a theme switch
Box and whisker color.
median_color
"#rrggbb"
"#000000"
Median line color.
outlier_color
"#rrggbb"
the box color
Outlier point color.
name
string
no name; automatic legends skip the mark
Legend name.
x_scale
{{ expression }}
the area's x scale
X scale. Discovered categories append to the band scale on the category axis.
Category column splitting rows into one series per distinct value (data sources only). Each series draws as its own line, takes a color from color_scale or the category palette in first-seen order, and contributes its own legend item.
color_scale
{{ expression }}
the category palette
Category color scale assigning per-series colors; requires series_column.
readout
"interpolate" | "nearest"
"interpolate"
Crosshair readout interpolation mode.
points
{{ expression }}
the data or view source
Inline points, an array of vec2s.
data
{{ expression }}
the points or view source
Bind to a tabular data source.
view
{{ expression }}
the points or data source
Bind to a streaming data view.
x_column
string
the call raises with data
X column name of a data source.
y_column
string
the call raises with data
Y column name of a data source.
x_scale
{{ expression }}
the area's x scale
X scale.
y_scale
{{ expression }}
the area's y scale
Y scale.
color
"#rrggbb"
the theme's chart-label color, restyled on a theme switch
Mark color.
name
string
no name; automatic legends skip the mark
Legend name.
max_points
number
every point is kept
Most points an inline-points mark keeps: past it, each Mark:push_point drops the oldest.
{min, max} output dot sizes in pixels for size_column values.
color_column
string
every dot takes color
Column driving per-dot color.
color_scale
{{ expression }}
the call raises with color_column
Color scale for color_column. A ramp whose domain is pending fits the column's extent once the data loads, and again when Mark:replace_data swaps the data.
points
{{ expression }}
the data or view source
Inline points, an array of vec2s.
data
{{ expression }}
the points or view source
Bind to a tabular data source.
view
{{ expression }}
the points or data source
Bind to a streaming data view.
x_column
string
the call raises with data
X column name of a data source.
y_column
string
the call raises with data
Y column name of a data source.
x_scale
{{ expression }}
the area's x scale
X scale.
y_scale
{{ expression }}
the area's y scale
Y scale.
color
"#rrggbb"
the theme's chart-label color, restyled on a theme switch
Mark color.
name
string
no name; automatic legends skip the mark
Legend name.
max_points
number
every point is kept
Most points an inline-points mark keeps: past it, each Mark:push_point drops the oldest.
Data-space position of the line. A category name resolves to the band scale's slot center when the mark is created; raises when the axis scale isn't a band scale or the category is unknown.
color
"#rrggbb"
the theme's chart-label color, restyled on a theme switch
A text annotation on the enclosing chart at a data-space (x, y). x / y / offset_x / offset_y are read as literal numbers (a templated value falls back to 0), like the other inline-data props.
Whether the font may join signs such as >= or -> into one shape. False draws each character as typed, in the text and in any element inside the label.
max_lines
number
no line limit
The most lines the label shows, at least 1. A longer text ends in "..." on the last line, cut at a glyph; a word wider than the label breaks at a glyph. Also a read/write property.
overflow_wrap
"break_word" | "normal"
"break_word"
How the label breaks a word wider than itself; max_lines and marquee break their own way while set. Also a read/write property.
Link text (required) — a plain string, or a datumhue.i18n.t(...) message that follows the active locale.
url required
string
–no default
Destination URL (required). Must be http, https, or mailto; any other scheme raises. Clicking opens it in a new browser tab — there is no programmatic open, so a user gesture is always required.
A selectable list. Provide its rows either by binding items to a Lua array via {{ }}, or by listing inline item children whose labels become the rows (children take priority).
A button that opens a file/directory picker on click; the positional argument is the label. Give it an id and bind on_pick from the hosting app via book:element.
Button label (required) — a plain string, or a datumhue.i18n.t(...) message that follows the active locale.
mode required
"file" | "files" | "dir"
–no default
What the click picks; selects the on_pick payload.
filters
{{ expression }}
every file shows
File-type filters, an array of {label, {ext, ...}} pairs. Valid only for file modes; raises with a directory mode.
start_dir
{{ expression }}
the dialog's own starting location
Seeds the dialog's starting location. Best-effort: native dialogs open there; browser pickers ignore it.
title
string
the dialog's own title
Dialog title.
on_pick
{{ expression }}
–no default
Called on a successful pick with the picked handle (never on cancel). Optional; bind later with elem:on_pick (e.g. from a hosting app via book:element). With no handler bound a click opens no dialog.
enabled
boolean
#true
Whether the button starts enabled. Setting false grays it out and ignores clicks.
Which side of the anchor to place it. Auto-flips to the opposite side if it would clip the window.
align
"start" | "center" | "end"
"start"
Alignment along the axis perpendicular to side.
visible
boolean
#false
Whether it starts shown; toggle later via the visible property.
light_dismiss
boolean
#false
Close the popover on a pointer press outside it. Off: fully app-controlled via the visible property. On: light-dismisses like a menu (pressing the anchor still toggles it).
navigation
"auto" | "modal" | "none"
"auto"
How it scopes focus navigation in your app. Light dismiss works the same in each.
A radio group. Provide its options either by binding options to a Lua array via {{ }}, or by listing inline radio children whose labels become the options (children take priority).
A button that opens a save dialog on click; the positional argument is the label. Give it an id and bind on_save / on_save_source from the hosting app via book:element.
Initial text content. Read/write later via the text property.
placeholder
string
the empty field shows no placeholder
Placeholder shown while the field is empty.
accessible_label
string | Message
the input has no name of its own
The name assistive technology announces for the input; its placeholder is not announced in its place. A datumhue.i18n.t(...) message follows the active locale. Also a read/write property.
leading_icon
{{ expression }}
no icon
An image drawn inside the field's border at its start, one text line tall, as a mask in the theme's secondary text color; a press on it focuses the field. Also a read/write property.
clearable
boolean
#false
Show a clear button at the field's end while it holds text. Raises on a multi-line input. Also a read/write property.
max_length
number
no length limit
Maximum number of characters the field accepts; a negative number sets no limit.
multiline
boolean
#false
Allow multiple lines: Enter inserts a newline instead of submitting.
tab_spaces
number
Tab moves focus, as on every other input
On a multi-line input, Tab types this many spaces at the caret, or over a selection of part of a line. With a whole line or several lines selected, it adds them to the start of each of those lines that is not empty. Shift+Tab takes up to as many off the start of each line the selection or the caret is on. Escape leaves the input. Raises on a single-line input, or for a number below 1 or above 16.
syntax
"lua"
plain text in the input's text color
Colors the text as source code of this language as it is typed, in the theme's syntax_* colors. In a multi-line input, Enter starts the new line indented as the line above it; a single-line input still calls on_submit on Enter. A disabled input shows its text in one color.
line_numbers
boolean
#false
Numbers the lines in a column at the input's start that scrolls with the text; a wrapped line is numbered on its first row. After the numbers the column keeps room for one more character, where line_marks show their signs. Raises on a single-line input, with leading_icon, and when padding gives the start a percentage.
ligatures
boolean
#true
Whether the font may join signs such as >= or -> into one shape. False draws each character as typed, in the text, the placeholder, the line numbers and any element inside the input.
filter
string
every character can be typed
A Lua character-class pattern (e.g. "%d", "[%d.-]") restricting which characters can be typed or pasted, matched per character. It masks the character *set*, not structure: validate the whole value in on_change or on_submit (and prefer number_input for numbers). ASCII-oriented like Lua's own %-classes; raises if the pattern is malformed.
purpose
"text" | "search" | "email" | "url" | "number"
"text"
What the field holds, which picks a browser's on-screen keyboard and the label of its Enter key. A filter that takes only digits, or digits and both decimal separators, wins: the field keeps its digit keypad and the purpose changes nothing. It never limits what can be typed; outside a browser it changes nothing.
select_all_on_focus
boolean
#false
Select all the text when the field gains keyboard focus.
enabled
boolean
#true
Whether it starts enabled. A disabled input ignores keyboard and pointer input until elem.enabled = true.
on_change
{{ expression }}
–no default
Called with the new text (string) on each user edit. A programmatic elem.text = ... write does not fire it.
on_submit
{{ expression }}
–no default
Called with the text (string) when Enter is pressed in a single-line input.
autofocus
boolean
#false
Take keyboard focus as soon as the input is created, as focus() does: created while its app is not the input owner, it moves no keys and is the focus the app gets back once it is. If several inputs request it in one frame, the last one created wins.
on_focus
{{ expression }}
–no default
Called when keyboard focus enters the input, as on_focus.
on_blur
{{ expression }}
–no default
Called when keyboard focus leaves the input (clicking away, Tab, or the app losing focus), as on_blur.
on_cancel
{{ expression }}
–no default
Called when a cancel request reaches the focused input: Escape (also while datumhue.ui.focus_navigation is off), gamepad B or Back. The input then loses focus, so the next request goes on past it.
Shared by every visual element: the same names and values as the fields of ui.NodeStyle. Omitted keys keep the element's own layout and theme style.
Attribute
Type
Description
left
number | string
Left anchor (px or "%").
right
number | string
Right anchor (px or "%").
top
number | string
Top anchor (px or "%").
bottom
number | string
Bottom anchor (px or "%").
width
number | string
Width (px or "%").
height
number | string
Height (px or "%").
min_width
number | string
Minimum width.
max_width
number | string
Maximum width.
min_height
number | string
Minimum height.
max_height
number | string
Maximum height.
aspect_ratio
number
Width over height. An automatic width or height follows the other size through the ratio, and a minimum or maximum set on only the other axis carries over to it.
margin
number | string
Outer margin: scalar, string, or {top,bottom,left,right}; a table's missing sides are 0.
padding
number | string
Inner padding: scalar, string, or {top,bottom,left,right}; a table's missing sides are 0.
border
number | string
Border widths: scalar, string, or {top,bottom,left,right}; a table's missing sides are 0.
Overflow handling. The scroll variants make the element a wheel-scrollable container; see the scroll / scroll_max element properties.
display
"flex" | "grid" | "block" | "none"
Display mode. grid places children in the columns grid_template_columns gives, one column without it.
grid_template_columns
number
The columns of a grid element. Its children fill them a row at a time, in child order, and a short last row keeps its columns' widths. A count, from 1 to 10000, gives that many columns sharing the width equally. A list gives one column per entry: a UI length other than "auto", "max_content" for a column as wide as its widest child, or "<n>fr" for n shares of the width the other columns and the gaps leave. A shared column never narrows below 0 px, so content wider than it overflows it. {} leaves one column.