Tags and attributes¶
Every tag and attribute the .lmn markup parser accepts, what it does, and
what it defaults to. For the style surface see CSS; for the
task-level introduction see Writing markup.
Markup is XML-shaped. Every tag closes (<tile/> or <tile></tile>),
attribute values are double-quoted, and a single element wraps the
document (<root> by convention). An unknown tag is a compile error. An
unknown attribute is dropped with a warning, so forward-compatible markup
still parses and a typo is still reported. Attributes on a custom widget tag
are exempt: the widget reads its own props out of them.
Element nesting is capped at 32 levels; a template instantiating other templates is capped at 64 levels of nesting.
Value forms¶
| Form | Accepted | Notes |
|---|---|---|
| length | auto, <n>, <n>px, <n>% |
A bare number is pixels. |
| number | <n>, <n>px |
The px suffix is accepted and stripped. |
| integer | <n> |
|
| color | #rrggbb, #rrggbbaa |
Named colours and rgb() are not accepted. |
| gradient | linear-gradient(...), radial-gradient(...), conic-gradient(...) |
bg only. See CSS values. |
| image | url("path"), url('path'), url(path) |
bg only. The path resolves against the app directory. See background images. |
| edges | 1 to 4 terms, top-right-bottom-left | 8, 8 16, 8 16 4, 8 16 4 12. Each term may be <n>, <n>px, or <n>%. |
| duration | <n>ms, <n>s |
|
| signal | name or $name |
The $ prefix is the preferred spelling. |
Boolean attributes¶
Every boolean attribute reads the same set of values:
| Value | Result |
|---|---|
true, yes, 1, or an empty value (disabled="") |
true |
false, no, 0 |
false |
| anything else | false, with a compiler warning naming the accepted forms |
Matching is case-sensitive, so True warns.
Text content and interpolation¶
An element's text child becomes its text: <label>Hello</label> is the
same as <label text="Hello"/>. An explicit text attribute wins, and
only the first non-empty text child is read.
{...} placeholders are substituted in text, placeholder, src,
id, class, style, and drag-payload:
| Placeholder | Resolves to |
|---|---|
{$name}, {name} |
The global signal name. |
{row.field} |
A field of the current <for> record. |
{$index} |
The current <for> row index. {idx} is the older spelling. |
Bare {name} still works; the compiler emits an informational lint
suggesting the explicit form.
A placeholder is resolved when the tree it belongs to is built, against the
signals as they stand then; a <for> row is built per record and reads the
signals then, and again whenever its record changes. One naming a signal
nothing has set stays in the string as the braces the author wrote, so a typo
reads as a typo. A {row.field} the record does not carry is the exception:
it resolves to an empty string and is reported, because the record is what
says which fields there are.
A {k} placeholder inside a <template> body reads the parameter k, and
falls back to the global signal k when no use site binds it. See
Composition.
Common attributes¶
These apply to any element unless the entry says otherwise.
Identity¶
| Attribute | Value | Effect |
|---|---|---|
id |
text | Names the element for CSS #id, script lookup, and <menu> targeting. |
class |
space-separated names | Names for CSS .class matching. |
style |
typography role | Sets font-size from a role name when font-size is not authored. Roles: display-xl, display-lg, display-md, display-sm, headline-lg, headline-md, headline-sm, title-lg, title-md, title-sm, body-lg, body-md, body-sm, label-lg, label-md, label-sm, caption, overline. |
tab-index |
integer | Keyboard focus order, and makes the element focusable by a click. -1 removes the element from Tab order; a click still focuses it. |
dir |
ltr, rtl, auto |
Writing direction; inherited by descendants. Defaults to the app locale's direction. |
lang |
BCP-47 tag | Language for text shaping and accessibility; inherited. |
translatable |
catalogue key | Resolves every string the element shows through the loaded translation catalogue: the text from the message the key names, placeholder from <key>.placeholder, and alt from <key>.alt. An attribute the markup does not write is not translated. The text falls back to the authored text, then to the key itself, unless the element names one of the other strings. lumenc i18n extract collects these keys. |
format |
number, currency:<code>, date, time, datetime, relative |
Writes the element's text for the app's locale: a decimal number; a decimal amount in an ISO-4217 currency; a YYYY-MM-DD date, optionally with THH:MM[:SS]; or whole seconds from now, past negative. Applies to a bind-text value as well as an authored text. A spec that is not one of these, or text the spec cannot read, leaves the text alone. |
Sizing¶
| Attribute | Value | Default |
|---|---|---|
width, height |
length | content size |
min-width, min-height, max-width, max-height |
length | unconstrained |
aspect-ratio |
number | none |
grow |
number | 0; <spacer/> defaults to 1 |
shrink |
number | 1 |
Spacing¶
| Attribute | Value | Default |
|---|---|---|
padding |
edges | 0 |
margin |
edges | 0 |
gap |
number | 0 |
inset |
edges; any side may be auto |
0 on <overlay> and <dialog>, otherwise auto |
Layout¶
| Attribute | Value | Default |
|---|---|---|
align |
start, end, center, stretch |
stretch |
justify |
start, end, center, between (space-between), around (space-around), evenly (space-evenly) |
start |
position |
relative, absolute |
relative |
z-index |
integer | paint in document order |
overflow, overflow-x, overflow-y |
visible, hidden, scroll |
visible |
layout-boundary |
boolean | Auto: true for scroll containers, and for elements that carry a fixed width, a fixed height and shrink: 0 |
Flex direction comes from the tag, not an attribute: <row> lays out
horizontally, <column> vertically. Use CSS flex-direction to change
it, including the reverse variants.
overflow: scroll on any element makes it a live scroll container, the
same as <scroll>.
An element's own text lays out as an item of its own, sized to the words,
so justify places it along the main axis the same way it places a child:
justify="center" on a <button> centres its label. It applies on a
horizontal main axis only, and an authored text-align wins where both
are set.
Colour and paint¶
| Attribute | Value | Default |
|---|---|---|
bg |
color, gradient, or url() image |
transparent |
bg-fit |
cover, contain, fill, none, scale-down |
cover |
radius |
number | 0 |
border |
<width> [solid\|none] <#color> in any order, or none |
none |
shadow |
[inset] <x> <y> <blur> [<spread>] <#color> |
none |
opacity |
number, clamped to 0..1 | 1 |
text-color |
color | inherited, else the built-in text color |
hover-bg |
color | no hover swap |
press-bg |
color | no press swap |
focus-outline |
<width> <#color> |
none |
disabled-opacity |
number, clamped to 0..1 | 0.5, used when the element is disabled and neither :disabled { bg } nor :disabled { opacity } was authored |
knob-color |
color | built-in knob fill |
knob-inset |
number | 4 |
thumb-size |
number | 16 |
popup-gap |
number | 4 |
The markup shadow attribute takes one shadow. Stacked shadows are a CSS
feature.
Typography¶
| Attribute | Value | Default | Inherited |
|---|---|---|---|
text |
text | none | no |
font-size |
number | 16 |
yes |
font-family |
family list | platform sans-serif | yes |
font-weight |
normal (400), bold (700), or 1..1000 |
400 |
yes |
line-height |
number (multiplier) or <n>px |
runtime ratio | yes |
text-align |
start/left, center, end/right |
start |
yes |
wrap |
none/nowrap, word/normal, glyph/char, ellipsis |
none |
yes |
max-lines |
non-negative integer | unlimited | yes |
wrap="ellipsis" elides overflowing single-line text with a trailing
.... It is the markup spelling of CSS text-overflow: ellipsis.
Text input paint¶
Only meaningful on <input> and <textarea>.
| Attribute | Value | Default |
|---|---|---|
selection-color |
color | text fill at reduced alpha |
selection-text-color |
color | selected glyphs keep their fill |
caret-color |
color | the text fill |
caret-width |
number | 2 |
caret-blink |
duration | 530ms half-cycle, shared by the whole app |
password-character |
one character | a bullet |
Scrolling¶
sensitivity and inertia apply to scroll containers. The
scrollbar-* attributes style the overlay scrollbar.
| Attribute | Value | Default |
|---|---|---|
scroll |
x, y, both |
y on <scroll>, otherwise no scrolling |
sensitivity |
number | 1.0 |
inertia |
number | 0.4 |
scrollbar-thickness |
number | 8 |
scrollbar-thickness-thin |
number | 4 |
scrollbar-margin |
number | 2 |
scrollbar-min-thumb |
number | 24 |
scrollbar-track-hover |
color | no track tint |
scrollbar-hover-boost |
number | 1.6 |
scrollbar-fade-delay |
duration | 1s |
scrollbar-fade-duration |
duration | 250ms |
Setting scroll="x" also flips a column-defaulted container to a row so
children stack left to right.
Interaction and drag and drop¶
| Attribute | Value | Effect |
|---|---|---|
disabled |
boolean | Input routing skips the element and the default render dims it. |
draggable |
boolean | Pointer drags translate the element. |
drag-payload |
text | Makes the element an in-app drag source publishing this payload. An empty value uses the element's id. Interpolated per row inside <for>. |
drop, drop-target |
boolean | Makes the element a drop target for OS file drops and in-app drags. |
accept |
MIME type | Restricts what an in-app drop target accepts. Absent accepts anything. |
An element receives the pointer when it is focusable (it has a
tab-index, which the controls and <a href> carry by default), when it
scrolls, or when it paints a background. Painting is not required: a
<button> with no bg in markup, in your CSS, or in a skin still
hovers, presses, and clicks over its own rect. Where several targets
overlap, the innermost one takes the event.
Binding¶
Values name a signal, with or without a leading $.
| Attribute | Applies to | Direction |
|---|---|---|
bind-text |
any element with text; <input>, <textarea> |
two-way for text inputs, one-way otherwise |
bind-checked |
<toggle>, <switch>, <checkbox> |
two-way |
bind-value |
<slider>, <progress>, <dropdown>, <tabs>, <date-picker>, <time-picker> |
two-way |
bind-disabled |
any | one-way, drives the disabled state |
bind-scroll |
scroll containers | two-way, vertical offset in pixels |
bind-text, bind-checked, and bind-value share one slot, so an
element binds at most one of them. bind-disabled and bind-scroll are
independent and may be combined with the others.
bind-text, bind-checked, and bind-value also accept $self.<field>
and $parent.<field> to read a per-entity property bag instead of a
global signal. bind-disabled and bind-scroll accept named signals
only.
No bind-* accepts $arg.<name>, the form that names a template
argument; it is refused with an error. An argument is substituted once,
when the instance is created, so a value that changes while the app runs
belongs in a signal the body binds to.
A widget seeds its bound signal from the authored value on first spawn if the signal is not already set.
Built-in sizing floors¶
An always-on stylesheet applies these beneath any skin and beneath your CSS, so a bare app still has usable controls. Any rule of yours wins.
root,title-bar:width: 100%;title-baris 32 px tall.button:min-height: 36.input,textarea:min-height: 24,min-width: 160.toggle:height: 36,min-width: 96.switch:height: 28,min-width: 52when neitherwidthnormin-widthis set.slider:height: 36,min-width: 160.checkbox,radio:min-height: 24.progress:width: 100%,height: 6..checkbox-box,.radio-dot: 18 by 18..split-pane:min-width: 0,min-height: 0..split-gutter:flex-basis: 6, neither growing nor shrinking..split-gutter-row:cursor: col-resize;.split-gutter-column:cursor: row-resize.
<input> and <textarea> also default to overflow: hidden on both
axes unless you author overflow, overflow-x, or overflow-y.
Containers¶
<root>¶
The document element. Lays out as a column and fills the window.
| Attribute | Value | Effect |
|---|---|---|
skin |
default, macos, windows, linux, auto |
Loads a built-in skin beneath your CSS. auto picks the skin for the current OS. |
frameless |
boolean | Removes the OS window frame. Pair with <title-bar>. |
In a multi-page app, skin and frameless are read from the home page only.
Every other attribute on a page's <root> styles that page. See
multi-page apps.
<column>¶
Stacks children vertically.
<row>¶
Stacks children horizontally.
<tile>¶
A plain box with no layout defaults. The usual choice for a styled surface.
<div>¶
Identical to <tile>. Both are boxes with no per-tag defaults.
<spacer>¶
A box that defaults to grow="1", so it absorbs the remaining space on
the main axis and pushes its neighbours apart.
<scroll>¶
A scroll container. Defaults to a vertical column scroller; scroll="x"
switches to a horizontal row scroller and scroll="both" enables both
axes. Accepts sensitivity, inertia, bind-scroll, and the
scrollbar-* attributes. A scroll container is a layout boundary.
Its automatic minimum size is zero on both axes, as for
overflow: scroll in CSS: a <scroll grow="1"> in a fixed-height
column stays at the height it is given and scrolls the rest instead of
growing to its content. Write min-height (or min-width) to set a
floor.
Its direct children default to shrink: 0, so content taller (or wider)
than the container keeps its own size and scrolls instead of being
squeezed to fit. Write shrink: 1 on a child to opt back in.
<overlay>¶
Floats out of normal flow: position: absolute with all four insets at
0, so it covers its parent's box. It is not reparented; it stays where
it is written, which is what a dropdown anchored to its trigger wants.
Lays out as a column. Use it for backdrops, dropdowns, and floating
panels.
An <overlay> under a parent that is content-sized and has no in-flow
children covers nothing, because that parent measures zero. See
Positioning.
<split>¶
Panes the user resizes. Each child is a pane, and a gutter goes between every two of them; dragging a gutter with the pointer moves the boundary between the two panes beside it.
<split class="workspace">
<column class="tools" min-width="120"/>
<column class="canvas" grow="1"/>
<column class="layers" min-width="160"/>
</split>
The panes lay out along the split's flex-direction: a row unless a style
sets flex-direction: column, which stacks them and makes the gutters
horizontal. A gutter never takes a pane below its min-width
(min-height in a column) or past its max-width (max-height). The
built-in floor makes every pane's own minimum zero, so a pane without a
min-width can be dragged down to nothing.
A gutter is focusable and is a separator, the web's window splitter.
While it is focused, ArrowLeft and ArrowRight (ArrowUp and ArrowDown in a
column) move it by 10 px, Home gives the pane before it its smallest size
and End its largest. It carries aria-orientation (vertical between
panes side by side), and aria-valuenow, aria-valuemin and
aria-valuemax: the share of its two panes the pane before it takes, in
percent, and the least and the most the panes' limits let it take.
A resize sets the panes' flex-basis, holding it over what the styles
say. The first resize holds every pane at the size it has, and later ones
move size from one neighbour to the other, so the panes' total stays the
same. The panes keep their own grow and shrink, so when the split
itself changes size they share the difference as their styles say. The
sizes are not saved or bound to a signal.
The panes carry the class split-pane and the gutters split-gutter, and
every skin styles the gutters by that class, :hover and :focus
included. A gutter also carries the class of its split's axis,
split-gutter-row or split-gutter-column, which follows the split's
flex-direction as it lays out; the pointer over a gutter shows
col-resize in a row and row-resize in a column. The gutter's thickness
is its flex-basis, 6 by default. A
<for> or an <if> written as a child of the split is one pane. On the web
the split, its panes and its gutters are the same elements, and the page's
app does the dragging and the keys.
Text and media¶
<label>¶
Text. Carries no layout defaults of its own; the text attributes above control its appearance.
<a>¶
An anchor. Clicking it navigates the app to another page. An anchor takes
the pointer with or without a background, and takes focus when clicked,
but it is not in the Tab chain unless you give it tab-index="0".
| Attribute | Value | Effect |
|---|---|---|
href |
page path | Target page, resolved against the app's .lmn files at navigation time. |
See Pages.
<image>¶
Loads and draws an image file.
| Attribute | Value | Effect |
|---|---|---|
src |
path | Image file to decode, relative to the app directory. Accepts {...} placeholders. |
alt |
text | What the image shows, for a reader who is not looking at it. Write alt="" for an image that carries no meaning of its own, such as a divider. translatable on the same element resolves it through <key>.alt. Carried into the compiled app; the desktop accessibility tree does not read it yet. |
fit |
fill, cover, contain, none, scale-down |
How the image fills its box. |
On the web an <image> is emitted as an <img> carrying the size of the
file the build copied, decoding="async", and loading="lazy" on every
image after the first one on the page. See
the web target.
<canvas>¶
A surface a script or a Rust app draws on: paths, rectangles, arcs, text,
and pixel buffers. The drawing functions come from the canvas namespace; see
Rhai, Lua, or
candela for the catalog. A Rust SDK app draws through
the lumenui-canvas crate instead; see Canvas.
The tag comes from the lumen-canvas module, so an app declares it before
it can write one:
In a page, the same declaration brings the module's web half, which draws the element with the browser's own canvas.
| Attribute | Value | Effect |
|---|---|---|
id |
text | Names the canvas. Every drawing call takes it as its first argument, so a canvas without one cannot be drawn on. |
width |
px | Drawing width, in canvas units. Defaults to 300. |
height |
px | Drawing height, in canvas units. Defaults to 150. |
width and height say what coordinates a script draws in. They are also
the element's default size, and layout is free to give it a different box:
flex-grow, a percentage width, or a min or max size all do. When the box
differs, the drawing is scaled onto it, so one script draws the same picture
at any size. An axis with no declaration draws in 300 units across or 150
down, which is what an HTML canvas has always defaulted to.
Setting width in CSS changes the drawing space, because a width in CSS
and a width in markup are the same declaration by the time the element
exists. That is a difference from the HTML canvas, where the attribute is the
drawing space and CSS scales it. The declaration is read once, when the
element is first laid out, so a later theme or media change does not resize
the drawing space under a script that is mid-drawing.
The drawing space is the element's, so a script reads it once the element is
mounted: on_ready, not on_start. Drawing from on_start works either
way; the calls are kept and appear when the element does.
A canvas is retained. What a script draws stays until canvas::clear, or
until canvas::resize, which empties it the same way writing width on an
HTML canvas does. Drawing outside the element's box is clipped.
A canvas composites like the HTML canvas: every globalCompositeOperation
(the Porter-Duff operators and the blend modes), clip and clear_rect, and
get_image_data to read what it composed back into a pixel buffer. A
composite operation works on the canvas's own pixels, never on the page
behind the element. In a web build the module's web half draws the same
calls with the browser's Canvas 2D.
Not here yet: gradients, patterns, shadows, measure_text, stroke_text,
text alignment and baselines, ellipse and arc_to, dashed strokes, drawing
an image file or another canvas directly, and a per-canvas device pixel
ratio. On the GPU renderer, copy, source-in, source-out,
destination-in and destination-atop under a clip also clear a band up to
16 pixels wide just outside the clip's edge.
Controls¶
<button>¶
A focusable, clickable box. Focusable by default (tab-index="0").
| Attribute | Value | Effect |
|---|---|---|
default |
boolean | Marks the default button of the containing <dialog>: Enter anywhere in the dialog activates it, and closing through it takes the accepted path. Also adds the default class. |
A button holds its label itself rather than in a child element, so
justify="center" is what centres the text.
<input>¶
A single-line text field. Focusable by default. Starts with an empty
buffer, so a text child is ignored; use value (as in HTML) or text for
an initial value.
| Attribute | Value | Effect |
|---|---|---|
placeholder |
text | Shown while the field is empty. Accepts {...} placeholders. translatable on the same element resolves it through <key>.placeholder. |
multiline |
boolean | Accept newlines. Defaults to false. |
pattern |
text | The value is valid only if it contains this literal substring. Not a regex. Values starting with shape: are reserved for the built-in checks <date-picker> and <time-picker> attach. |
required |
boolean | The value is valid only if it is non-empty. |
autofocus |
boolean | Takes focus when the containing <dialog> opens. |
password-character |
one character | Masks the rendered value. |
value |
text | The initial text, or the initial number or color of a typed field. |
type |
text, number, color |
What the field holds. Defaults to text. Any other value is a text field, as it is in a browser, and the parser warns. |
min and max also feed validation on <input> and <textarea>.
type="number"¶
A field for a number, with the attributes and behaviour of HTML's number input.
| Attribute | Value | Effect |
|---|---|---|
value |
number | The initial number. |
min, max |
number | The range the steps stay in, and the range a typed value must be in to be valid. |
step |
number, any |
The step. Defaults to 1. any steps by one and accepts any number. |
ArrowUp and ArrowDown step the focused field, so does the wheel while the
pointer is over the focused field (the page under it does not scroll then),
and so do its - and + buttons. Each step writes the new number into the
field as an edit, so input handlers and a bound signal see it. A step works
the way a browser's does: a value between steps moves to the next step in
that direction, a value on a step moves by one step, and the result stays
within min and max. Steps start at min, or at 0 without one, and an
empty field steps from 0.
The field holds the number as text. It is valid while it is empty (unless
required) or holds a number within min and max that is a whole number
of steps from the start, and invalid otherwise. Validity lands in the
valid:<id> signal. Typing is not filtered: a letter is accepted and leaves
the field invalid, where a browser refuses it.
The field carries the class number-input. Its step buttons sit in a
.number-steps box along its right end, which the field keeps clear of its
text, and are .number-step-down and .number-step-up. On the web the field
is the browser's own <input type="number">, with its own step control and
none of these parts.
type="color"¶
A color field, with the value and events of HTML's color input: it holds
one sRGB color, written as HTML writes a color well's value. Without
alpha that is #rrggbb in lowercase. With alpha the color has an alpha
too and is written color(srgb r g b / a), each component from 0 to 1, the
/ a left out for an opaque color: color(srgb 0 1 0 / 0.5).
| Attribute | Value | Effect |
|---|---|---|
value |
color | The initial color. Defaults to opaque black. |
alpha |
boolean | The alpha can be picked too, and the value carries it. Without it every color is made opaque. |
bind-value |
signal | Holds the value: the field writes it on every change and follows it when a script writes it. A signal nothing wrote yet starts at the field's color. |
A value, a typed color and a color a script writes to the bound signal are
read as #rgb, #rgba, #rrggbb, #rrggbbaa or color(srgb r g b / a).
Anything else is opaque black, as a browser reads a value it cannot parse.
Named colors and rgb() are not read.
The field is a compact face showing the color. A click, Enter or Space opens
its panel below it (above it near the bottom of the window), and a click
outside the field or Escape closes it. In the panel, the square picks
saturation (across) and brightness (down) at the current hue, the strip
below it picks the hue, with alpha a second strip picks the alpha from
clear (left) to opaque (right), and the text field takes a color typed as
#rrggbb or rrggbb on Enter, or #rrggbbaa with alpha. Every change
raises input with the new value, and closing the panel on a color other
than the one it opened with raises change, as a browser's picker does.
Every part is styled by class. The face is .color-input and its swatch
.color-swatch; the panel is .color-panel, holding .color-area with its
.color-area-thumb, .color-hue with its .color-hue-thumb, with alpha
the .color-alpha strip (its backdrop) holding the .color-alpha-fill
shading and the .color-alpha-thumb, and the .color-hex field. The area's
shading and the hue strip are gradients in the user-agent sheet. On the web
the field is the browser's own <input type="color">, which opens the
browser's or the system's picker, and none of these parts are in the page.
The page's input carries alpha, which a browser that does not implement it
ignores: Chrome today shows an opaque picker and reports #rrggbb.
<textarea>¶
Same as <input> with multiline defaulting to true.
<toggle>¶
A checkbox-style boolean control rendered as a track with a knob. Focusable by default.
| Attribute | Value | Effect |
|---|---|---|
checked |
true, yes |
Initial state. |
Style the checked track with :checked { bg: ... } and the knob with
knob-color and knob-inset.
<switch>¶
The same boolean control in switch presentation: a narrower pill track
with a sliding thumb. It reports itself to assistive technology as a
switch. Same attributes as <toggle>.
<slider>¶
A draggable value control. Focusable by default.
| Attribute | Value | Default |
|---|---|---|
value |
number | min |
min |
number | 0 |
max |
number | 1 |
step |
number | (max - min) / 100 |
Arrow keys move by one step, Page Up and Page Down by ten. Style the
thumb with knob-color and thumb-size.
A click or a drag lands on the same positions the arrow keys reach: the
value is min plus a whole number of steps. A range that is not a whole
number of steps stops on the last step that fits, so min="0" max="100"
step="30" tops out at 90. The value carries no more decimal places than
min and step do, so min="0.8" step="0.1" writes 0.9 to a bound
signal.
<checkbox>¶
A row containing an indicator box and a caption. Focusable by default.
The parser synthesizes the two parts, so CSS can reach them as
.checkbox-box and .checkbox-label. On the web the indicator is the
browser's own control and carries the same class, so one rule styles
both targets.
| Attribute | Value | Effect |
|---|---|---|
label |
text | The caption. Text content works too. |
checked |
true, yes |
Initial state. |
indeterminate |
boolean | Tri-state dash until the first user toggle. |
Defaults to align="center" and gap="8" when you do not set them.
<radio>¶
One member of an exclusive group. The group's selected value lives in a
signal. Synthesized parts are .radio-dot and .radio-label; on the web
the dot is the browser's own control under the same class.
| Attribute | Value | Effect |
|---|---|---|
group |
signal name | Required. The signal holding the group's selected value. |
value |
text | Required. This member's value. Unlike other tags, value is a string here. |
label |
text | The caption. |
checked |
true, yes |
Seeds the group signal with this member's value. |
A radio group is one Tab stop, and the arrow keys move within it. On the
desktop Lumen keeps exactly one member of each group in the Tab order; on
the web the members share the browser's own name group, and the browser
does the same. A tab-index written on a <radio> has no effect on either
target, because the group decides where its members sit in the Tab order.
An empty group or value puts no name or value on the browser's
control, so on the web a radio with an empty group belongs to no group.
<progress>¶
A non-interactive progress track. Lays out as a row and fills its
container's width. The parser synthesizes a .progress-fill child.
| Attribute | Value | Effect |
|---|---|---|
value |
number | Determinate progress. Omit it for an indeterminate sweep. A bar with bind-value and no value sweeps until its signal first holds a number, then shows it. |
max |
number | Upper bound. Defaults to 1. |
duration |
integer milliseconds | Indeterminate sweep period. Defaults to 1200. |
chunk |
number 0..1 | Fraction of the track the indeterminate chunk covers. Defaults to 0.3. |
<dropdown> and <option>¶
A combobox. The parser expands it into a header button plus a floating options panel; the panel is dismissed by clicking outside it and flips above the trigger near the bottom of the window.
The tag stays on the box those parts sit in, so width, min-width,
padding, bg and a dropdown rule in a stylesheet size and paint the
control the way they do any other widget. The generated elements carry
the classes dropdown-button for the closed face, dropdown-panel for
the floating list, and dropdown-option for a row in it. The face fills
the box, so sizing the <dropdown> sizes the whole control; the face's
height and an option row's are theme metrics, so restyle those classes to
change them.
<dropdown> requires bind-value. Its children must all be <option>.
| Tag | Attribute | Effect |
|---|---|---|
<dropdown> |
bind-value |
Required. Signal holding the selected value. |
<dropdown> |
placeholder |
Header text while the signal is empty. Setting it also suppresses the first-option seed, so the dropdown starts with nothing selected. |
<option> |
value |
Required. Value written to the signal on click. |
<option> |
label |
Display text. Defaults to value. |
<option> |
disabled |
Unclickable, skipped by arrow navigation. |
The closed header reads the selected option's label. The signal holds
that option's value, which is what your script reads and what a click
writes, so the two differ whenever an option carries a label of its own.
A value no option declares is shown as it stands.
The first <option> seeds the signal, so the dropdown opens on a real
selection. Add placeholder to start unselected instead. A value your
script writes first always wins over the seed.
The closed dropdown handles Up and Down to step the value, Alt+Down, Space, or Enter to open, and type-ahead.
<option> outside a <dropdown> is an error.
<tabs> and <tab>¶
A tabbed container. The parser expands it into a button strip plus one body per tab; bodies are mounted once and shown or hidden, so focus and scroll state survive switching.
<tabs> requires bind-value. Its children must all be <tab>.
| Tag | Attribute | Effect |
|---|---|---|
<tabs> |
bind-value |
Required. Signal holding the active tab name. |
<tab> |
name |
Required. The value written when the tab is picked. |
<tab> |
label |
Strip button text. Defaults to name. |
<tab> |
disabled |
The strip button is unclickable and skipped by arrow navigation; the body still mounts if a script activates the tab. |
The first <tab> seeds the signal, so it is active at startup. The
generated elements carry the classes tabs, tab-strip, and tab-btn.
<tab> outside <tabs> is an error.
<date-picker> and <time-picker>¶
A validated text field for a date or a time. Each expands to an <input>
with a built-in pattern and the class date-picker or time-picker. Like
any <input> it is in the Tab order, and the other attributes written on
the picker (id, class, tab-index, required, sizing) pass through to
the generated input.
| Attribute | Value | Effect |
|---|---|---|
bind-value |
signal | Required. |
placeholder |
text | Defaults to YYYY-MM-DD for dates and HH:MM for times. |
width |
length | Defaults to 180px. |
The built-in check matches the placeholder: YYYY-MM-DD with month 01-12
and day 01-31 for a date, HH:MM with hour 00-23 and minute 00-59 for a
time. Digits and separators must sit in those positions, ignoring
surrounding whitespace. It is a shape check, not a calendar check, so
2026-02-31 passes.
Validity lands in the valid:<id> signal like any other validated
field, so give the picker an id to read the result.
Structure¶
<for>¶
Repeats its children once per item of an array signal. Lays out as a column.
| Attribute | Value | Effect |
|---|---|---|
each |
array signal name | Required for iteration. Without it the children spawn once, unrepeated. |
key |
field name | Record field used as the reconciliation key. Without it the item index is the key. A row whose key is kept is updated in place when its record's fields change. |
virtualized |
true, yes |
Spawn only the rows in the visible scroll window. Needs a <scroll> ancestor. |
row-height |
number | Pixel height per virtualized row. Defaults to 32. |
Inside the body, {row.field} reads the current record and {$index}
the row index. A virtualized <for> is forced to full width so its rows
have a reference box.
<if>¶
Mounts its children conditionally.
| Attribute | Value | Effect |
|---|---|---|
signal |
signal name | The gate. Truthy means set, non-empty, and not false or 0. |
eq |
text | Mount only when the signal equals this value, instead of testing truthiness. |
mode |
render, hide |
render (default) despawns and respawns the body on each toggle. hide mounts once and toggles visibility, preserving focus, scroll, and per-row state. |
<template>, <use>, and <slot>¶
<template name="Card">...</template> declares a reusable subtree.
Instantiate it as <Card k="v"/> or <use template="Card" k="v"/>.
Every {k} placeholder in the body is a parameter, and takes the value
the use site binds to k. Attributes on the <template> tag itself
(other than name) are defaults for the parameters a use site leaves
unbound; a parameter with neither reads the global signal k.
Placeholders resolve in attribute values and text, not in tag names.
<slot/> inside a template body is replaced with the content of the use
site. When the use site has none, the slot falls back to its
default="..." attribute and then to its own inner content.
Giving the use site an id prefixes every id inside the instantiated
body with it, so multiple instances keep distinct ids. Templates may
instantiate other templates, up to 64 levels; a cycle is an error.
A declaration is visible to the whole file, wherever it sits in it, to any file that includes that file, and app-wide in a multi-page app.
The name of a candela component instantiates the same way: <Home name="bob"/>
takes the lmn! block the function Home returns. Any component can be named
this way. Where the block is the function's whole body and reads only its
parameters, the build puts it in the tree outright; where the function works a
value out or picks between blocks, the build leaves a marker there and the
runtime fills it by calling the function on the first tick. Component names and
<template> names share one namespace, and two
declarations claiming one name are an error. See
composition.
<include>¶
<include src="../parts/header.lmn"/> splices another file's markup in
place. Paths are relative to the including file. Includes resolve before
parsing, they nest, and a cycle is an error. Included files are watched for hot reload.
<script>¶
Collects script source. It is not a layout element and can appear anywhere in the tree.
| Attribute | Value | Effect |
|---|---|---|
src |
path | Script file to load, relative to the app's src/ directory. |
Without src, the tag's text content is the script. Each src file runs
under the host its extension names (.cdl, .rhai, .lua); sources of one
language are concatenated into one program. A tag with a body has no extension
to read and joins the app's one external language, or candela when there is not
exactly one. [script] engine in lumen.toml overrides all of it and puts
every source on one host. See Scripting.
Shell¶
<menubar>¶
Declares the application menu. At most one per document, as a direct child
of <root>. Its children must be <menu label="...">.
On macOS and Windows it is the OS's own menu bar, and it takes no place in
the layout tree. On Linux, where desktops have no menu bar for an app to
join, and on every platform when lumen.toml sets
[window] menubar = "window", the same markup is
drawn in the window: a bar at the top of the root,
one button per menu, each opening its menu as a popup. The bar floats over
the root's top edge and the root keeps its height clear above the content,
so the root's justify and align place the content as they would without
it. Its parts carry the classes menubar (the bar), menubar-menu (one
per menu), and menubar-button (the menu's label), and the menus are the
.menu-panel popups described below. While one of its menus is open,
moving the pointer to another button opens that menu instead.
<menu>, <menuitem>, and <separator>¶
Inside <menubar>, <menu label> is a top-level menu and its children
are <menuitem> and <separator>.
| Tag | Attribute | Effect |
|---|---|---|
<menu> |
label |
Required inside <menubar>. Menu title. |
<menu> |
id |
Required outside <menubar>. Names the popup. |
<menuitem> |
id |
Required. Identifies the item to scripts. |
<menuitem> |
label |
Display text. Defaults to id. |
<menuitem> |
accel |
Keyboard accelerator, such as CommandOrControl+S or Alt+F4. Menubar menus only. The in-window bar runs it while the window has focus. |
<menuitem> |
disabled |
Unclickable, skipped by arrow navigation. Popup menus only. |
Outside a menubar, <menu id="m"> is an in-window popup panel toggled by
the signal __menu_open:m. It expands to a floating panel of buttons
with the classes menu-panel, menu-item, and menu-separator, and it
dismisses on an outside click.
<menuitem> and <separator> outside a menu are an error.
<dialog>¶
A modal overlay: absolutely positioned over its parent's box, centered,
with its children as content. Focus is trapped inside it while it is
open. Write it as a direct child of <root>, which is what makes it
cover the window; under a content-sized parent it covers nothing (see
Positioning).
| Attribute | Value | Effect |
|---|---|---|
open |
signal name | Shows the dialog while the signal is truthy. The body is mounted once and hidden, so its state survives. |
Inside a dialog, autofocus picks the element that takes focus on open,
and <button default="true"> is the default button.
<tooltip>¶
Wraps exactly one trigger element and shows a popup after the pointer
dwells on it. The wrapper collapses at parse time, so CSS selectors apply
to the trigger, not to <tooltip>.
| Attribute | Value | Default |
|---|---|---|
text |
text | empty |
translatable |
catalogue key | none; the popup body is the key's message, falling back to text |
delay |
integer milliseconds | 500, or the --lumen-tooltip-delay custom property |
offset |
number | 12, or the --lumen-tooltip-offset custom property |
Wrapping zero or more than one element is an error.
The popup is a tooltip element with the class tooltip, placed under
the root, so a .tooltip rule styles it like any element: background,
text color, padding, radius, border, shadow, and font. It inherits from the
root, which puts the root's custom properties in scope, and it paints above
everything else. Its position is the runtime's: it opens beside the pointer
and moves to the pointer's other side near the window's edges, so inset
and position in a .tooltip rule have no effect. Scripts do not see it,
and it does not count as one of the root's children for :last-child and
the other structural selectors.
In a web build the tooltip is the trigger's title attribute, which the
browser shows in its own look.
<title-bar>¶
A title-bar region for frameless windows. Lays out as a row, full width, 32 px tall.
| Attribute | Value | Effect |
|---|---|---|
drag |
boolean | Pressing and moving the bar requests a native window drag. |
Pair it with <root frameless="true">.
Custom tags¶
A Rust plugin can register additional tags, which then parse like
built-in ones. An element written with a #[derive(Widget)] tag becomes that
widget, its props read from the element's attributes. See
Authoring plugins.