Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

text

One paragraph drawn in the fonts chain or a named family: labels, clocks, notification bodies. It sizes to its content, wraps and elides inside a bounded width, and sets weight and italic style for the whole node, and mixes bold, italic, colour and links through runs. For typing, use a textfield.

Two notification cards. In the first, the title elides and the body wraps to two lines, eliding the second; the second card’s short texts fit.

local function card(icon_name, title, body)
    return row {
        width = "fill",
        padding = 12,
        spacing = 10,
        radius = 12,
        background = "#313244",
        children = {
            icon { name = icon_name, size = 32, foreground = "#CDD6F4", align_v = "center" },
            column { width = "fill", align_v = "center", spacing = 2, children = {
                text { content = title, width = "fill", font_size = 14, elide = "end" },
                text { content = body, width = "fill", foreground = "#A6ADC8",
                       wrap = "word", max_lines = 2, elide = "end" },
            } },
        },
    }
end

return column { width = 340, spacing = 8, children = {
    card("dialog-information-symbolic", "Firmware update ready for the USB-C dock",
        "3 packages can be installed. Restart to finish the kernel upgrade and load the new graphics driver."),
    card("battery-caution-symbolic", "Battery low", "12% left"),
} }

The middle column is "fill" so the texts have a bounded width; the icon keeps its 32 px.

These node-level styles change line spacing, character spacing, and the selected font face:

return column { width = 320, padding = 16, spacing = 10, radius = 12,
    background = "#313244", children = {
    text { content = "Default: One line\nSecond line", font_size = 16 },
    text { content = "Taller lines: One line\nSecond line", font_size = 16,
           line_height = 1.7, foreground = "#A6E3A1" },
    text { content = "Wide letters", font_size = 16, letter_spacing = 3,
           foreground = "#89B4FA" },
    text { content = "Bold italic", font_size = 16, font_weight = 700,
           italic = true, foreground = "#F9E2AF" },
} }

Properties

text takes the common properties, plus:

PropertyTypeDefaultBehaviour
contentstring|TextRun[]|Bound""A string, or an array of up to 10000 runs, drawn as one paragraph
fontstring|BoundThe fonts chainFamily placed before the fonts chain. "" raises; an unknown family falls back to the chain
font_sizenumber|Bound, [1, 8192]12Text size in logical pixels. A textfield sizes its placeholder with it
line_heightnumber|Bound, [0.1, 10]1.2Line height as a multiple of font_size
letter_spacingnumber|Bound, [-100, 100]0Extra space between characters in logical pixels. Negative values tighten text
font_weightnumber|Bound, [1, 1000]400Font weight from 1 to 1000. A run with bold = true uses weight 700
italicboolean|BoundfalseUse the family’s italic face when available. A run with italic = true stays italic
font_variationstable<string, number>|Bound{}OpenType variation axes by 4-character tag ({ FILL = 1, GRAD = -25, opsz = 24 }), as CSS font-variation-settings. Values clamp to each face’s range; axes a face lacks are ignored. An explicit wght overrides font_weight and bold runs. Changes snap; see variable fonts
foregroundColor|Bound"#FFFFFF"A colour; a run’s color overrides it, and a textfield’s placeholder takes it unless placeholder_color is set
text_align"start"|"center"|"end"|Bound"start"Aligns lines inside the node’s own box; "start"/"end" follow each line’s reading direction. Matters only when the box is wider than the text
wrap"none"|"word"|Bound"none""word" breaks at words, mid-word when one word is too wide. Needs a bounded width (width, "fill" or a stretched cross axis)
max_linesnumber|Bound0Line cap under wrap = "word"; 0 is unlimited, a negative value is refused. Ignored without wrap
elide"none"|"end"|"start"|"middle"|Bound"none"Replaces what does not fit with …: "end" keeps the start, "start" the end, "middle" both. Under wrap it applies to the last kept line as "end"
elidedBoundNoneAn elided(name) signal; layout writes whether elide or max_lines removed content
on_linkfun(href: string)NoneClick on a run with an href; the engine never opens it. Takes the click from any on_click, the text’s own included; plain words pass it on

Runs

Each run in a content array is a table:

FieldValuesDefaultBehaviour
textStringRequiredA run without it is refused; an empty one is skipped
bold, italicBooleanfalsebold uses weight 700; italic enables italic. Other node-level styles carry through
underlineBooleanfalseUnderline in the run’s colour
colorColourThe node’s foreground
hrefStringNoneHanded to on_link on click; the pointer shows "pointer" over it. "" is no link

A run also takes kind = "text", so a notification body’s text spans pass through unchanged. Drop its image spans, which have no text. A nil hole ends the array.

local body = text {
    width = 280,
    wrap = "word",
    content = {
        { text = "Update ready. " },
        { text = "3 packages", bold = true },
        { text = " can be installed. " },
        { text = "Release notes", underline = true, color = "#89B4FA", href = "https://example.org/notes" },
    },
    on_link = function(href) process.detach("xdg-open", { href }) end,
}

return body

Variable fonts

font_variations sets a variable font’s axes, as CSS font-variation-settings does. Keys are 4-character tags and case matters: registered axes are lower case (wght, wdth, opsz, slnt), a font’s own axes upper case (FILL, GRAD).

local starred = state("starred", false)
local star = text {
    font = "Material Symbols Rounded",
    font_size = 24,
    content = "\u{e838}",
    font_variations = starred:map(function(on) return { FILL = on and 1 or 0, GRAD = 0, opsz = 24 } end),
}
RuleBehaviour
Measure and paintBoth use the same axis values, so advances and outlines agree
RangeA value outside the face’s range clamps to it
Missing axisIgnored, so a fallback face that lacks it draws as before
wghtOverrides font_weight and bold runs; without it, font_weight sets wght
ChangeSnaps: animate does not tween it

Size

A text node measures its content: one line per paragraph line, line_height × font_size each, as wide as the widest line. The default ratio is 1.2. letter_spacing changes both glyph placement and the width used for wrapping. wrap and elide need a box narrower than the text, so give the node a width, "fill", or a stretched cross axis (a text in a fixed-width column wraps at the column’s width). In a content-sized row, the text measures one line and overflows instead.

line_height sets layout only: glyph ink that runs past a tight line box, such as a descender, paints outside it. Only the node’s width and an ancestor’s clip cut it.

Bind an elided(name) signal as elided to detect content removed by elide or max_lines. Wrapping alone keeps it false. Elision cuts at grapheme boundaries, preserving combining marks and emoji sequences. elide = "start" keeps the end of the line and "middle" keeps both ends, which suits file names whose extension matters; the end side takes the odd grapheme. Both cut in logical order, so “start” is the start of the string even in right-to-left text, and both act as "end" under wrap. With wrap = "word", max_lines = N and elide = "end", the preview keeps the first N shaped lines and adds … to the last retained line. It shortens that line only as needed to fit the ellipsis; it never merges content from later lines into the preview.

How do I…

TaskAnswer
Truncate a long titlewidth (or "fill") plus elide = "end"
Keep a file extension visiblewidth plus elide = "middle"
Show at most two lineswrap = "word", max_lines = 2, elide = "end" and a bounded width, as in the card
Bold one wordA run with bold = true
Make a clickable linkA run with href plus on_link on the node (process.detach to open it)
Use an icon font glyphfont = "Symbols Nerd Font" (any installed family) with the glyph as content
Fill a Material Symbols iconfont_variations = { FILL = 1 } (variable fonts)
Centre text in a fixed-width boxtext_align = "center" with a width
Show a live valueBind content to a signal: content = volume:map(function(v) return v and tostring(v) or "" end)

Gotchas

TrapFix
A wrap = "word" text runs off the edge on one lineWrapping needs a bounded width: set width, "fill", or put it in a fixed-width column. A content-sized row offers none
elide never ellipsizesSame cause: the box is as wide as the text. Bound the width
max_lines has no effectIt applies only under wrap = "word"
text_align = "center" does nothingThe box is exactly as wide as the text. Give it a width, or centre the node with align_h
font = "" raisesOmit font to use the chain
A link run is not clickableLinks need on_link on the same text; without it the click goes to the on_click around it
content = 42 raisescontent takes a string or runs: tostring(n)

See also: textfield, fonts, icon.

Source: vocabulary, content parsers, measure, line height, link hit testing.