Markup tag reference¶
Lumen's authoring surface is a subset of XML-shaped markup with a fixed
tag vocabulary. Unknown tags fail-fast at parse time with the byte
offset. The live list is the KNOWN_TAGS array in
lumenc/src/parser_html.rs;
this page tracks every entry.
This page groups tags by family. For attribute semantics that work on
every tag (id, class, width, padding, bg, etc.) see the
CSS subset — the same names work as inline markup attrs.
Common attribute families¶
All visible tags accept the layout + visual attributes. Repeated here only when a tag interprets them in a non-default way.
| Family | Attributes |
|---|---|
| Layout box | width, height, min-width, min-height, max-width, max-height, aspect-ratio, padding, margin, gap, grow, align, justify, position, inset, overflow, overflow-x, overflow-y |
| Visuals | bg, radius, shadow, opacity |
| Text | text, text-color, font-size, text-align, wrap, max-lines, style (named typography role) |
| Interaction | hover-bg, press-bg, focus-outline, tab-index, draggable, drop |
| Bindings | bind-text, bind-checked, bind-value, bind-scroll |
| Reactivity | each, key, signal, mode, eq (per-tag — see <for> / <if>) |
Value grammar (lengths, edges, colors) lives in
lumenc/src/values.rs.
A condensed reminder:
- Length: bare number is px (
24),Npx(24px),N%(50%). - Edges (padding / margin / inset): 1, 2, or 4 numbers (CSS shorthand).
- Color:
#rrggbbor#rrggbbaa. bg: a color ORlinear-gradient(...)/radial-gradient(...)/conic-gradient(...).
<root>¶
The single document root. Every .lmn file has exactly one. Defaults
to a vertical-flex layout filling the window.
Synopsis. Single root of the layout tree; skin opts into the
embedded user-agent stylesheet, frameless removes window decorations.
Attributes.
| Attr | Type | Notes |
|---|---|---|
skin |
identifier | e.g. skin="default" — opt into the embedded user-agent stylesheet. Equivalent to [skin] name in lumen.toml. |
frameless |
bool | frameless="true" removes the title bar / window chrome. Pair with <title-bar drag="true"> to keep window dragging. |
class |
identifier list | Same class semantics as elsewhere; CSS rules can target the root. |
<root skin="default" class="app theme-dark">
<column padding="24">
<label text="Hello" />
</column>
</root>
Related. <title-bar>.
<column> / <row>¶
Flex containers. <column> stacks children top-to-bottom; <row>
stacks left-to-right. Defaults: align="stretch", justify="start".
Attributes. Layout family. gap controls spacing between
children. grow is set on children to claim leftover axis.
<column padding="24" gap="12" align="center">
<label text="Heading" />
<row gap="8" justify="end">
<button text="Cancel" />
<button text="OK" />
</row>
</column>
Related. <spacer>, <tile>.
<spacer>¶
Empty flex grow box. Defaults to grow="1" so it eats remaining axis.
Use to push siblings to opposite ends of a row / column.
<tile>¶
Generic styled box leaf. Carries no built-in behaviour; use as a paint target for backgrounds, gradients, shadows.
Related. <button> (focusable tile with click dispatch).
<scroll>¶
Clipped scrolling container. Auto-applies layout-boundary (taffy
sub-tree isolation) so its inner content can be relayouted without
invalidating the rest of the tree.
Attributes.
| Attr | Type | Notes |
|---|---|---|
scroll |
y | x | both |
Default y. x flips the inner flex to row automatically. |
sensitivity |
number | Wheel-delta multiplier. 1.0 = default; 0.5 halves the scroll speed. |
inertia |
number | 0..1 friction factor. 0 = no inertia; higher = longer glide. |
layout-boundary |
bool | true by default for <scroll>. |
bind-scroll |
signal name | Two-way f32 binding (logical px) to the vertical offset. |
<scroll height="320" sensitivity="0.6" inertia="0.4">
<for each="todos" key="id" gap="6">
<row class="list-row">
<label width="32" text="{row.idx}" />
<label grow="1" text="{row.label}" />
</row>
</for>
</scroll>
A focused <scroll> region (or the nearest scrollable ancestor of the
focused element) also responds to the keyboard: arrow keys scroll by a
line, PageUp / PageDown by a page, and Home / End jump to the
ends. Horizontal arrows are ignored on a vertical-only scroller, and a
focused text input keeps its own arrow handling instead of scrolling.
bind-scroll="signal" makes the offset reactive without any per-frame
script hook: writing the signal (f32, logical pixels) scrolls the
container on that tick — out-of-range values clamp to the content
extent, exactly like user scrolling — and cancels any in-flight fling.
The other direction is throttled: user scrolling writes the offset back
into the signal once, when the scroll settles (offset stopped moving
and the fling slept), never on every frame.
<scroll height="320" bind-scroll="feed_pos"> … </scroll>
<button text="Back to top" on-click="scroll_top" />
<script>
fn scroll_top(id) { signal("feed_pos", 0.0).set(0.0); }
</script>
Related. <for>.
<overlay>¶
Floats out of normal flow. Defaults: position="absolute",
inset="0 0 0 0", so it covers its nearest positioned ancestor
(typically the root). Use for modal backdrops, dropdowns, tooltips —
anything that should paint above its siblings.
<root>
<column> ... main content ... </column>
<overlay class="dim">
<tile width="200" height="200" bg="#ffffffcc" />
</overlay>
</root>
Related. <dialog> (modal sugar over <overlay>).
<label>¶
Text leaf. Renders text through cosmic-text; respects font-size,
text-color, wrap, max-lines, text-align.
Attributes.
| Attr | Type | Notes |
|---|---|---|
text |
string | Body text. Can also be the element's text content (<label>Hi</label>). |
font-size |
number | px. Default 16. |
text-color |
color | Default inherits from CSS / root. |
wrap |
none | word | glyph |
Default none (no wrap). |
max-lines |
int >= 0 | Truncate to N lines with an ellipsis. |
text-align |
start | center | end |
aliases: left / right. |
style |
typography role | Named type scale (e.g. title-lg, body-md, caption) that sets a default font-size. See CSS subset. |
bind-text |
signal name | Replace text content with the named signal's value. |
Width / height defaults. taffy doesn't measure text.
<label>seedsheight=24(≈ one line at 16px font) andmin-width=80so a bare<label text="Hi" />is visible without explicit sizing.
<input> / <textarea>¶
Editable text field. <input> is single-line; <textarea> is
multiline (Enter inserts \n, Shift+Enter commits).
Attributes.
| Attr | Type | Notes |
|---|---|---|
placeholder |
string | Shown when content is empty. |
bind-text |
signal name | Two-way: typing writes back to the signal. |
multiline |
bool | Explicit override. <textarea> defaults to true; <input> defaults to false. |
disabled |
bool | Marks the field disabled — the runtime Disabled marker gates input, and CSS :disabled routes its fill. |
required |
bool | Validation flag — empty content fails. |
pattern |
string | Validation — content must contain this literal substring (not a regex; full regex not yet wired). |
min / max |
number | Numeric range when content parses as a number. |
<input width="280" placeholder="Type something…" bind-text="who" />
<textarea width="320" height="80"
placeholder="Multi-line — Shift+Enter to commit"
bind-text="note" />
<!-- Validation. valid:<id> signal mirrors the result. -->
<input id="email" required="true" pattern="@" bind-text="email" />
<label bind-text="valid:email" />
Pattern is literal-substring, not regex. It is a contains-check. A full regex backend is not yet wired; until then
pattern="@"matches any string containing the character.
Keyboard. Text fields support word-wise editing: Ctrl/Cmd+Left/Right
jumps by word, Ctrl/Cmd+Backspace/Delete deletes the previous /
next word, and Ctrl/Cmd+A selects all. Plain Left/Right, Home, and End
move the caret; holding Shift extends the selection. On <textarea>
(multiline), Up/Down move the caret one VISUAL line (soft-wrap aware) while
keeping a sticky target column, and Home/End jump to the visual line
start/end (not the \n-delimited logical line); Ctrl/Cmd+Home/End
jump to the document start/end. A bare Enter inserts a newline and
Shift+Enter commits; on single-line <input>, Enter commits.
Pointer + IME. A click (or drag, double-click word, triple-click line) hit-tests against the real shaped glyphs, so the caret lands on the correct character even in proportional fonts. When an OS input method is active, the candidate window docks under the caret (not the whole field).
Related. <date-picker>, <time-picker> (validated <input> collapses).
<image>¶
Raster or SVG image. The asset is loaded once and cached
(MemoryBudget.images_mb).
Attributes.
| Attr | Type | Notes |
|---|---|---|
src |
path | Relative paths resolve against the app dir, then each [asset_roots] path. |
fit |
fill | cover | contain | none | scale-down |
Default none. |
opacity |
0..1 | Wraps the draw in push_layer(alpha) when < 1. |
SVG limitations. The SVG walker handles linear + radial gradients and clip-paths. Pattern / mask / nested-image / text nodes currently warn and skip.
<div>¶
Generic block leaf. Renders as a tile-with-no-default-decoration.
Mostly useful when you want a class-styled box without <tile>'s
default sizing.
<button> / <toggle> / <slider>¶
Stateful controls. Each is focusable (tab-index=0 by default) and
hit-tested for pointer interaction.
<button>¶
A focusable tile with click dispatch.
Inner text content becomes the button label. Default size: 36 px tall,
≥ 96 px wide (so a bare <button text="…" /> is visible without
explicit sizing).
<toggle>¶
Boolean two-state control. The visual is a checkbox / switch hybrid.
bind-checked="signal" is two-way: the user click writes back via the
BindChecked reverse-listener. A <toggle> also accepts disabled to
gate interaction, and CSS :checked / :disabled route its fill.
<slider>¶
0..1 (or min..max) drag value.
bind-value="signal" is two-way. The dragged value is clamped to the
[min, max] range (defaults: 0 and 1). A focused slider is also
keyboard-driven: ←/→ (and ↑/↓) nudge by one step, PageUp / PageDown
by ten steps, and Home / End jump to min / max. The mouse wheel
over a hovered slider nudges by one step per notch (and never scrolls an
ancestor <scroll> container), and Escape mid-drag cancels the drag,
restoring the pre-drag value. step="…" sets the keyboard / wheel
increment; absent, it defaults to (max - min) / 100.
<checkbox> / <radio> / <progress>¶
W5 form controls. All three desugar in the parser to real element subtrees, so every visual (indicator size, colors, radii, sweep timing) is CSS-reachable through the skins or app CSS.
<checkbox label="…">¶
Box + label boolean control on the same machinery as <toggle>:
checked, bind-checked (two-way), fires on_toggle(id, checked).
Clicking anywhere on the row — box, label, or gap — toggles, as does
Space while focused. Desugar:
checkbox (row, Toggleable)
├─ .checkbox-box — indicator tile; ✓ / – mark rendered as text
└─ .checkbox-label — the caption
<checkbox id="opt" label="Enable telemetry" bind-checked="telemetry" />
<checkbox label="Partially synced" indeterminate="true" />
<checkbox label="Locked" disabled="true" />
indeterminate="true" is the web/Qt tri-state: the box renders a dash
(over the checked fill) regardless of checked until the FIRST user
toggle clears it. Script bind-checked writes do not clear it. Style
via .checkbox-box { … }, checkbox:checked { bg: … } (the box fill
when on), checkbox:focus { outline: … }.
<radio group="…" value="…" label="…">¶
Name-grouped exclusive choice (Qt auto-exclusive radio group / ARIA
radiogroup). All <radio> elements sharing a group string form one
group; the group's selected value lives in the PropertyStore signal of
that name — read it from scripts like any signal, drive it to select
programmatically.
<radio group="ship" value="air" label="Air freight" checked="true" />
<radio group="ship" value="sea" label="Sea freight" />
<radio group="ship" value="rail" label="Rail" disabled="true" />
- Exactly one selected per group:
checked="true"seeds the signal; with none checked the first enabled member is auto-selected. - Click (row / dot / label) and
Spaceselect. ←/→/↑/↓move selection to the previous / next ENABLED member, wrapping at the ends and skipping disabled members; selection follows focus (Qt/GTK).- Roving tabindex: only the selected member sits in the Tab chain, so
Tabenters and leaves the whole group as one stop.
Style via .radio-dot { … }, radio:selected { bg: … } (dot fill),
radio:focus { outline: … }.
<progress value max>¶
Non-interactive progress bar (never focusable, consumes no input).
<progress value="30" max="100" width="240" /> <!-- determinate 30% -->
<progress bind-value="pct" max="1" /> <!-- signal-driven -->
<progress /> <!-- indeterminate sweep -->
With value / bind-value, the .progress-fill child's width tracks
value / max. Without either, the bar is INDETERMINATE: a 30 %-wide
chunk bounces across the track; the sweep period comes from
duration="ms" → CSS progress-duration → the
--lumen-progress-period token (skins default 1200 ms). A later
bind-value write flips an indeterminate bar to determinate. Track
styling on progress { … }, fill on .progress-fill { … }. Hidden
bars (closed tab, hidden dialog) stop animating and requesting frames.
<for each="…" key="…">¶
Reactive iteration. Spawns one copy of its inline children per item in
the named ArraySignal. Reference an item's fields inside the body with
the {row.field} form — {row.label}, {row.status}, and so on — and
each row resolves it against its own item at reconcile time.
Use
{row.field}, not bare{field}. A bare{field}inside a<for>body resolves against global signals first, not the row item, so it is ambiguous and the compiler warns on it. Always qualify row fields withrow.; use{$name}when you deliberately want a global signal from inside the loop.
Attributes.
| Attr | Type | Notes |
|---|---|---|
each |
signal name | Must reference an ArraySignal (via signal_array(name) in Rhai). |
key |
field name | Stable identity for reconciliation. The reconciler diffs by key. |
virtualized |
bool | Recycle row entities for long lists (see the note below). |
row-height |
number | Required when virtualized="true". |
<for each="todos" key="id" gap="6">
<row class="list-row" align="center" gap="10">
<label width="32" text="{row.idx}" />
<label grow="1" text="{row.label}" />
<label class="pill" text="{row.status}" />
</row>
</for>
let todos = signal_array("todos");
todos.set([
#{ id: "1", idx: "1", label: "Layout — taffy", status: "done" },
#{ id: "2", idx: "2", label: "Reactive signals", status: "done" },
]);
todos.push(#{ id: "3", idx: "3", label: "New row", status: "todo" });
Reconciler. Append-only + tail-trim fast paths preserve focus, scroll position, and per-row signals. Mid-stream insert / reorder still falls back to a full rebuild.
Virtualized lists.
virtualized="true"has the row-pool shape but hard-codes the scroll-window math; a general windowing solution is not yet wired.
<if signal="…" mode="render|hide">¶
Conditional subtree. Mounts its inline children only when the named
signal is truthy (non-empty AND not literal "false" / "0").
Attributes.
| Attr | Type | Notes |
|---|---|---|
signal |
signal name | Required. The signal to watch. |
mode |
render | hide |
render (default): spawn / despawn on flip. hide: spawn once + flip Visible. |
eq |
string | Optional. When set, the truthy check becomes signal == eq. Powers the <tabs> collapse. |
<!-- Spawn only when loaded -->
<if signal="loaded">
<column> ...body... </column>
</if>
<!-- Keep body's state across show/hide flips -->
<if signal="dialog-open" mode="hide">
<column> ...form fields...</column>
</if>
mode="hide" is the right default for any subtree whose descendants
hold non-trivial state (input cursors, scroll positions). mode="render"
saves the despawn / respawn ECS cycles for branches that don't.
Related. <dialog> (sugar over <if mode="hide">).
<dialog open="…">¶
Modal overlay. Sugar for an absolute-positioned full-viewport container
whose visibility is bound to a signal. Implements the Qt QDialog
contract (W5):
- Focus trap — Tab / Shift-Tab cycle only within the open dialog
(
FocusBoundary). - Initial focus — on open, focus moves to the first
autofocus="true"descendant; else the first focusable descendant in markup order; else the dialog panel itself. - Focus restore — on close, focus returns to whatever held it before the dialog opened.
- Default button —
<button default="true">(gains thedefaultclass for skin styling).Enteranywhere in the dialog activates it, except when focus sits on another button or a multiline textarea; single-line inputs fire the default alongside their own commit (Qt/web line-edit behaviour). With no explicit default, the first enabled button acts as the Enter target (Qt autoDefault). - Exactly-once accepted / rejected — every open→close cycle fires
exactly one of
on_dialog_accepted(id)/on_dialog_rejected(id)(per-id routerson("dialog_accepted", id, fn)/on("dialog_rejected", id, fn)). Accepted = the close went through the default button (click or Enter-anywhere); everything else — Escape, cancel buttons, script signal writes — is rejected.idis the dialog'sid="…", falling back to its open-signal name.
Attributes.
| Attr | Type | Notes |
|---|---|---|
open |
signal name | When the signal is truthy, the dialog mounts. |
id |
string | Handed to the accepted / rejected script hooks. |
<dialog id="confirm-dialog" open="dialog_open">
<column class="card" width="420" gap="12">
<label class="h2" text="Confirm" />
<input placeholder="Type to confirm" bind-text="confirm_text" autofocus="true" />
<row gap="10">
<button id="dialog-close" text="Cancel" />
<button id="dialog-confirm" text="Confirm" default="true" />
</row>
</column>
</dialog>
signal("dialog_open", ""); // initial: closed
on("click", "dialog-close", "close");
on("click", "dialog-confirm", "close");
on("dialog_accepted", "confirm-dialog", "on_ok");
on("dialog_rejected", "confirm-dialog", "on_cancel");
fn close(_id) { signal("dialog_open", "").set(""); }
fn open(_id) { signal("dialog_open", "").set("1"); }
fn on_ok(_id) { /* commit */ }
fn on_cancel(_id) { /* discard */ }
The dialog descendants' state (input text, toggles) survives show/hide
because the collapse uses <if mode="hide">.
<title-bar drag="true">¶
Frameless-window draggable region. Defaults: 32 px tall,
full-width row at the top. Adding drag="true" makes pressing and
moving the bar request a native winit::Window::drag_window().
<root frameless="true">
<title-bar drag="true">
<label text="My app" />
<spacer />
<button id="close" text="×" />
</title-bar>
<column> ...body... </column>
</root>
<tooltip text="…" delay="…">¶
Hover-delay popup. Wraps exactly one trigger child; the parser
collapses the <tooltip> wrapper and attaches a TooltipSource to
the inner element. At runtime lumen-primitives::TooltipPlugin
watches Hovered, stamps a per-entity HoverStartedAt, and spawns an
absolute-positioned popup once dwell exceeds delay_ms.
Placement is cursor-relative (Qt QToolTip): below-right of the
pointer hotspot by offset px, flipping above / left of the cursor
near the viewport's bottom / right edges so the popup never leaves the
window.
Attributes.
| Attr | Type | Notes |
|---|---|---|
text |
string | Tooltip body. |
delay |
int ms | Dwell time before show. Unset ⇒ the --lumen-tooltip-delay skin token (default 500; macOS skin 1000, Windows 400). |
offset |
px | Cursor-to-popup gap. Unset ⇒ --lumen-tooltip-offset (default 12). |
Multi-child tooltips are a parse error so authors get clear feedback instead of silent first-child pickup.
<tabs bind-value="…"> + <tab name label>¶
Tabbed container. Children must be <tab name="…" label="…">…</tab>.
The parser flattens to a column with a button strip on top and per-tab
<if mode="hide" eq="…"> bodies, so switching tabs preserves
descendant state.
<tabs> attributes.
| Attr | Type | Notes |
|---|---|---|
bind-value |
signal name | Required. Signal stores the active tab name. First tab seeds as default. |
<tab> attributes.
| Attr | Type | Notes |
|---|---|---|
name |
string | Required. Used as the equality value for the bound signal. |
label |
string | Optional. Visible button text; falls back to name. |
<tabs bind-value="active_tab">
<tab name="primitives" label="Primitives">
<row gap="12"> ...content... </row>
</tab>
<tab name="controls" label="Controls">
<column> ...content... </column>
</tab>
</tabs>
<tab> outside <tabs> is a parse error.
<dropdown bind-value="…"> + <option value label>¶
Select widget. The parser collapses to a header button + an
absolute-positioned options panel toggled via __dropdown_open:<signal>.
Clicking an option writes its value to the bound signal and closes the
panel. The panel also dismisses itself on Escape or on a press
outside its bounds.
<dropdown> attributes.
| Attr | Type | Notes |
|---|---|---|
bind-value |
signal name | Required. Current selection. |
placeholder |
string | Header text before any selection. |
<option> attributes.
| Attr | Type | Notes |
|---|---|---|
value |
string | Required. Written to the bound signal on click. |
label |
string | Optional. Visible text; falls back to value. |
<dropdown bind-value="weight" placeholder="Select weight…">
<option value="light" label="Light" />
<option value="medium" label="Medium" />
<option value="heavy" label="Heavy" />
</dropdown>
<option> outside <dropdown> is a parse error.
<menu id="…"> + <menuitem id label accel> + <separator/>¶
Popup / context menu. <menu> collapses to an absolute-positioned
panel toggled via __menu_open:<id>. Use open_menu(id) /
close_menu(id) from Rhai to flip the panel; item clicks fire
on_menu(id) and close the menu. Like the dropdown, the panel also
dismisses on Escape or an outside press.
<menu> attributes.
| Attr | Type | Notes |
|---|---|---|
id |
string | Required. The menu's id stem; the open-signal becomes __menu_open:<id>. |
<menuitem> attributes.
| Attr | Type | Notes |
|---|---|---|
id |
string | Required. Passed to on_menu(id) / a per-id handler. |
label |
string | Optional. Visible text; falls back to id. |
accel |
string | Optional (menu-bar only — see below). Accelerator hint. |
<menu id="actions">
<menuitem id="rename" label="Rename" />
<menuitem id="duplicate" label="Duplicate" />
<separator />
<menuitem id="delete" label="Delete" />
</menu>
on("click", "open-actions", "open");
on("menu", "delete", "do_delete");
fn open(_id) { open_menu("actions"); }
fn do_delete(_id) { /* ... */ }
<menuitem> or <separator> outside <menu> or <menubar> is a
parse error.
<menubar> + <menu label>¶
Native OS menu bar (macOS / Windows). <menubar> lives directly
inside <root>; it's extracted from the layout tree at parse time and
attached to the OS window via muda. Linux builds skip menu-bar
attach (libxdo dep is optional).
Inside <menubar>, <menu label="..."> blocks contain <menuitem> /
<separator> exactly like popup menus, but their id lands on the
native menu chain. Click dispatch fires on_menu(id).
<root>
<menubar>
<menu label="File">
<menuitem id="new" label="New" accel="Cmd+N" />
<menuitem id="open" label="Open…" accel="Cmd+O" />
<separator />
<menuitem id="quit" label="Quit" accel="Cmd+Q" />
</menu>
<menu label="Edit">
<menuitem id="undo" label="Undo" accel="Cmd+Z" />
</menu>
</menubar>
<column> ...body... </column>
</root>
Duplicate <menubar> blocks under one <root> are a parse error.
<date-picker bind-value="…"> / <time-picker bind-value="…">¶
Validated text inputs. Today they collapse to an <input> with a
built-in pattern substring matcher and an id so the valid:<id>
signal mirrors the result. A full grid picker is not yet wired.
Attributes.
| Attr | Type | Notes |
|---|---|---|
bind-value |
signal name | Required. |
id |
string | Optional. Used as the validation signal stem. |
placeholder |
string | Default: YYYY-MM-DD (date) or HH:MM (time). |
Pattern matchers (literal-substring): date requires -, time
requires :. The placeholder hints at the full shape.
<template name="…" {defaults}>¶
Reusable markup body with <slot/>, id auto-namespacing, and default
attribute values. Detailed in Templates + slots.
<template name="card" variant="primary">
<column class="card card-{variant}">
<slot />
</column>
</template>
<card> ...content... </card>
<card variant="danger"> ...content... </card>
<script> / <script src="../…rhai" />¶
Not a layout node — captured at parse time into LayoutIR.script_source.
Or inline (avoid XML-illegal characters):
Multiple <script> blocks concatenate in source order.
<include src="../…lmn" />¶
Splits markup across files. At parse time the referenced .lmn file is
loaded and its top-level elements splice in place of the <include>
tag — as if you had pasted the file's contents there. Not a layout node;
nothing survives in the tree but the included elements.
<root>
<column>
<include src="parts/header.lmn" /> <!-- header.lmn's elements land here -->
<label text="body" />
</column>
</root>
Paths resolve relative to the file that contains the <include>, so
a nested include inside parts/header.lmn resolves against parts/.
Templates register globally. A <template name="…"> defined in an
included file is usable from any file — the parser collects every
template across the whole include graph before expanding use-sites, so
inclusion order doesn't matter:
<!-- lib.lmn -->
<template name="card"><tile class="card"><slot/></tile></template>
<!-- main.lmn -->
<root>
<include src="lib.lmn" />
<card>hi</card> <!-- resolves to lib.lmn's template -->
</root>
Rules and limits:
- Nested includes are followed recursively.
- Cycles are rejected with an error naming the full chain
(
include cycle detected: main.lmn -> a.lmn -> b.lmn -> a.lmn). - A missing file is an error carrying the include-site position
(
include "parts/x.lmn" not found (from main.lmn:3:5): …). - Editing any included file hot-reloads the app, exactly like editing
main.lmn. - Both
<include src="../…"/>(self-closing) and<include src="../…"></include>(any body ignored) are accepted.
Includes are resolved by the runtime and lumenc check. Tooling that
parses raw strings without a file loader (e.g. the LSP when a document
isn't backed by a real path) drops unresolved <include> tags rather
than erroring, so single-file editing never breaks.
Default sizes / behaviours¶
Without explicit width / height taffy collapses zero-text-measure
children to 0 px. Lumen seeds tap-sized defaults so a bare control is
visible without sizing:
| Tag | Default height |
Default min-width |
|---|---|---|
<root> |
100% | — |
<title-bar> |
32 px | — |
<label> / <input> |
24 px (font_size × 1.5) | 80 / 160 px |
<button> / <toggle> / <slider> |
36 px | 96 / 96 / 160 px |
<spacer> |
— | — (carries grow=1) |
Override per-instance with explicit attributes. CSS rules can also set them, but inline attributes always win.
Reserved attribute names by tag¶
| Tag | Attribute | Meaning |
|---|---|---|
<if> |
signal, mode, eq |
Conditional subtree. Ignored on other tags. |
<dialog> |
open |
Sugar for signal=<v> mode=hide. |
<tabs> |
bind-value |
Required. |
<dropdown> |
bind-value, placeholder |
placeholder only here. |
<menu> |
id |
Required for popup menus. |
<menubar> |
— | Only valid directly inside <root>. |
<title-bar> |
drag |
Frameless-window drag region. |
<root> |
skin, frameless |
Window-level metadata. |
<image> |
src, fit |
Asset path + sizing. |
Unknown attributes are silently ignored today (forward-compat). A strict mode that surfaces them as parse errors is on the roadmap.