3A minimalist layout language inspired by elm-ui that compiles to static HTML.
7- `@` means structure, bare lines mean content.
8- Layout is explicit and compositional: every element declares its own layout
9 role, and there is no CSS cascade to reason about.
10- Output is one self-contained HTML file with embedded CSS (flexbox), and no
11 JavaScript unless you write some.
13Every example block in this file is compiled by the test suite
14(`tests/docs.rs`), so the examples stay correct.
21@element [attributes] argument
25Children are indented under their parent. Attributes are comma-separated inside
26`[...]` and may span several lines:
33 box-shadow 0 2px 4px rgba(0,0,0,0.1)
38An attribute is either a **style**, written `key value` (`padding 20`,
39`color red`), or an **HTML attribute**, written `key=value` (`id=main`,
40`type=email`, `aria-label=Close`). Boolean HTML attributes are written bare
41(`required`, `disabled`, `open`). A comma inside `(...)` or `"..."` does not
42split attributes, so a font stack is written `font "Inter, sans-serif"`.
46Any line that doesn't start with `@` or `[` is text. Text after an element's
47attributes is its content, and `{...}` puts elements inside a line of text:
51 This is {@text [bold] important} and this is a {@link https://example.com link}.
52@text [bold, size 24, color #333] Hello world
53@section [padding 8] Text after the attributes is content too.
58`--` at the start of a line begins a comment. Comments must be on their own
59line: `--` later in a line is ordinary text.
64 -- todo: add nav items
69A line starting with `[` is an anonymous `@el`. `>` chains single-child
70elements on one line; the last element in the chain gets the indented children.
73[padding 20, background white]
76@el [padding 16, background blue, rounded 8] > @link https://example.com
77 @text [color white] Get Started
82`@raw """ ... """` is pasted into the output verbatim.
86<div class="custom-widget"></div>
92Layout elements set up flexbox; the rest map to the HTML element of the same
95| Element | Output | Purpose |
97| `@row` | div, flex row | Horizontal layout |
98| `@column` | div, flex column | Vertical layout |
99| `@el` | div, flex column | Generic container |
100| `@grid` | div, grid | Grid container (use `grid-cols`) |
101| `@stack` | div, position relative | Children layered on top of each other |
102| `@in-front` / `@behind` | div, absolute | Overlay layers filling the parent (see below) |
103| `@text` | span | Styled inline text |
104| `@paragraph` | p | Flowing text with inline elements |
105| `@link url` | a | Link; text after the URL becomes its content |
106| `@image src` | img | Image |
107| `@h1` … `@h6` | h1 … h6 | Headings |
109Semantic containers, all laid out as columns: `@nav`, `@header`, `@footer`,
110`@main`, `@section`, `@article`, `@aside`, `@address`, `@search`, `@form`,
111`@details` / `@summary`, `@dialog`, `@figure` / `@figcaption`, `@blockquote` /
112`@cite`, `@fieldset` / `@legend`, `@noscript`.
114Content: `@list` / `@item` (`@list [ordered]` for `<ol>`), `@dl` / `@dt` /
115`@dd`, `@table` / `@thead` / `@tbody` / `@tr` / `@th` / `@td`, `@code`, `@pre`,
116`@hr`, `@mark`, `@kbd`, `@abbr`, `@time`, `@progress`, `@meter`, `@output`,
117`@canvas`, `@iframe src`, `@video src`, `@audio src`, `@picture` / `@source`,
118`@script`, `@fragment` (children without a wrapper).
120Form controls: `@input`, `@button`, `@select` / `@option`, `@textarea`,
121`@label`, `@datalist`. `@form URL` sets the form's `action`.
127@form [method=post] /subscribe
128 @label [for=email] Email
129 @input [type=email, name=email, id=email, required]
130 @button [type=submit] Send
136### `@in-front` / `@behind`
138Overlay layers, like elm-ui's `inFront` and `behind`. Their children fill the
139parent's bounds; the parent automatically becomes a positioning context
140(`position: relative; isolation: isolate`). An explicit `position` on the parent
144@el [width 200, height 200, background blue]
145 @text [color white] Main content
147 @text [color yellow] Painted on top
149 @el [background red, width fill, height fill]
154These components are written in htmlang (`std.hl`) and available in every file.
155Use them like elements; your own `@let` with the same name takes precedence.
157| Component | Purpose |
159| `@badge` | Small pill for counts and statuses |
160| `@tag` | Label with slightly rounded corners |
161| `@chip` | Outlined, fully rounded label |
162| `@avatar` | Circular frame for an image or initials |
163| `@spacer` | Takes up the remaining space in a row or column |
164| `@tooltip [tip TEXT]` | Text that shows `TEXT` when hovered |
165| `@carousel` | Horizontally scrolling row that snaps to each child |
166| `@breadcrumb` | Breadcrumb trail of `@item`s |
167| `$skeleton`, `$truncate`, `$no-scrollbar` | Attribute bundles: loading placeholder, one-line ellipsis, hidden scrollbars |
170@row [spacing 8, align-items center]
171 @badge [background #ef4444, color white] 3
174 @tooltip [tip Opens in a new tab] Help
178@el [$skeleton, height 20, width fill]
185| Attribute | Effect |
187| `spacing N` | Gap between children |
188| `padding N` / `padding Y X` / `padding T R B L` | Padding (also `padding-x`, `padding-y`) |
189| `margin ...` | Margin, same forms as padding (also `margin-x`, `margin-y`) |
190| `width fill` / `width N` / `width shrink` | Take remaining space, exact size, or fit content |
191| `height fill` / `height N` / `height shrink` | Same for height |
192| `center-x`, `center-y` | Center within the parent |
193| `align-left`, `align-right`, `align-top`, `align-bottom` | Align within the parent |
194| `wrap` | Let a row wrap |
195| `grid-cols N`, `grid-rows N`, `col-span N`, `row-span N` | Grid layout |
196| `hidden` | `display: none` |
200| Attribute | Effect |
202| `border N COLOR` (also `border-top` etc.) | Solid border |
203| `rounded N` | Border radius |
204| `bold`, `italic`, `underline` | Text style |
205| `size N` | Font size |
206| `font NAME` | Font family |
207| `line-clamp N` | Cut text off after N lines |
209**Any standard CSS property** can also be used as an attribute, with the same
210name and value: `background red`, `opacity 0.5`, `cursor pointer`,
211`z-index 10`, `margin-top 16`, `grid-template-areas "a b"`, and so on.
213**Units.** For lengths, a bare number is pixels (`padding 20` is `20px`).
214Values with a unit, keywords and CSS functions are passed through: `width 50%`,
215`margin 0 auto`, `max-width min(100%, 800px)`.
219Write HTML attributes as `key=value`: `id=main`, `class=note`, `href=/about`,
220`type=email`, `alt=Logo`, `target=_blank`, `aria-label=Close menu`,
221`data-id=42`. Any name works. Booleans are written bare: `required`,
222`disabled`, `checked`, `open`, `sandbox`. A style and an HTML attribute may
223share a name without clashing:
226@image [width=800, width 200, alt=A photo] photo.jpg
227@select [size=4, size 18]
231### State and media prefixes
233Prefix a style attribute to apply it conditionally:
237| `hover:`, `active:`, `focus:`, `focus-visible:`, `focus-within:`, `disabled:`, `checked:`, `visited:`, `target:`, `valid:`, `invalid:`, `empty:`, `placeholder:`, `selection:` | In that state |
238| `first:`, `last:`, `odd:`, `even:`, `nth:EXPR:` | By position among siblings |
239| `children:` | To each direct child |
240| `before:`, `after:` | On the `::before` / `::after` pseudo-element (with `content`) |
241| `has(SELECTOR):` | When the element contains a match |
242| `sm:`, `md:`, `lg:`, `xl:`, `2xl:` | From that viewport width up (640–1536px) |
243| `cq-sm:` … `cq-2xl:` | Container queries |
244| `dark:`, `print:`, `motion-safe:`, `motion-reduce:`, `landscape:`, `portrait:` | Media conditions |
247@el [padding 16, background #3b82f6, hover:background #2563eb, md:padding 32, dark:background #1e3a8a]
248 @text [color white] Click me
249@row [spacing 4, children:flex 1, nth:2n:background #f3f4f6]
252@el [before:content "→ ", before:color red]
256### Conditional attributes
258`if(CONDITION, A, B)` picks `A` or `B` by the condition. It works as a value or
259as a whole attribute, and an empty choice (the `B` can be left out) leaves the
264@el [background if($active, blue, gray), if($active, bold), padding if($active, 12)]
268## Variables and functions
270`@let` defines everything reusable. What it defines depends on its shape:
273-- A value, used as $primary
275-- A computed value: `=` makes it an expression
277-- A quoted string, with $variables interpolated
278@let greeting "Hello $primary"
279-- An attribute bundle, used as [$card]
280@let card [padding 20, background white, rounded 8]
281-- A function: a @let with an indented body
282@let panel $title $tone=neutral
287@panel [title Welcome]
288 Body text goes into @children.
289@el [$card, background #f9fafb]
290 Attributes after a bundle override it.
293A function is called like an element:
295- Its parameters are passed as attributes; parameters with `=default` may be
297- Other attributes style its root element, so `@panel [title Hi, padding 40]`
298 works like styling a built-in element.
299- Text after the attributes and indented children replace `@children`, and a
300 caller's `@slot name` block replaces `@slot name` (the slot's own children are
302- An `@style` block at the top of the body is scoped to the function: its rules
303 only apply inside the function's output.
308 .title { font-weight: bold; }
309 @el [padding 12, rounded 6, background #eff6ff]
310 @text [class=title] $kind
313@note [kind Tip, padding 20] Scoped styles and forwarded attributes.
316A multi-line string uses triple quotes; its indented lines are the value:
326`@let --name value` also emits a CSS custom property, used as `$--name` and
327as `var(--name)` in the generated CSS:
331@el [background $--brand] Themed
336`@assert CONDITION` fails the build when false. `@warn MESSAGE` emits a
341Conditions and computed values (`@let x = ...`) are expressions:
345| Values | numbers, `"strings"` (with `$var` interpolation), `$variables`, `true`, `false`, and bare words (`dark`, `#fff`) as strings |
346| Arithmetic | `+ - * / %` with the usual precedence, unary `-`, `( )` |
347| Comparison | `== != < > <= >=` (numeric when both sides are numbers), `contains`, `starts-with`, `ends-with` |
348| Logic | `and`, `or`, `not`; empty, `false` and `0` are false |
349| Choice | `if(CONDITION, A, B)` |
350| Text functions | `uppercase(s)`, `lowercase(s)`, `capitalize(s)`, `trim(s)`, `length(s)`, `reverse(s)`, `truncate(s, n)`, `replace(s, old, new)`, `default(s, fallback)` |
351| Color functions | `lighten(c, pct)`, `darken(c, pct)`, `alpha(c, a)`, `mix(c1, c2, pct)` |
353Variables are looked up while evaluating, so a value containing `==` or spaces
354is still one value. An invalid expression is a compile error.
356In text and attribute values, `$name` inserts a variable and `${EXPR}` inserts
357the value of any expression:
362@text [color ${darken($base, 10)}] ${uppercase($name)} has ${length($name)} letters
367All control flow runs at compile time.
370@let items apple, banana, cherry
373@if $count > 2 and not $hidden
383 @text The list is empty.
385@each $i in 1..10 step 3
388@each $label, $url in Home /, About /about
392A range counts down when its start is greater than its end.
396| Directive | Effect |
398| `@include file.hl` | Insert another file: its content and definitions (a library of `@let`s emits nothing) |
399| `@include lib.hl as ui` | Take only its definitions, named `ui.name` |
400| `@data file.json` / `@data $name file.json` | Load JSON values as variables (`$name.key`) |
401| `@data $name dir/*.json` | Load each file as `$name.STEM.key`; `$name` lists the stems |
402| `@data $name env:NAME [default]` | Read an environment variable |
403| `@markdown` / `@markdown file.md` | Markdown, converted to HTML |
404| `@image [inline] file.svg` | Inline an SVG file (`width`, `height`, `color`, `class=`, `id=` apply to it) |
406A layout is an ordinary function in its own file. It marks where content goes
407with `@slot name` (named blocks, with default content) and `@children`
408(everything in the call outside `@slot` blocks):
414 @column [max-width 800, center-x]
426 @paragraph This fills @children.
431`@page TITLE` produces a full HTML document; without it the output is a fragment.
432Its attributes set `lang`, `favicon`, `canonical` and `base`. `@meta NAME VALUE`
433adds a meta tag (`og:` names become Open Graph `property` tags), and
434`@head` holds any other raw HTML for the `<head>` (fonts, JSON-LD, a manifest
435link). Translations are a JSON file per locale: `@data $t locales/$lang.json`.
438@page [lang en, favicon /favicon.png] My Site
439@meta description A small site
440@meta og:title My Site
445- `@style` holds raw CSS, including at-rules such as `@scope`, `@property` and
447- `@keyframes name` takes htmlang attribute syntax (`from [opacity 0]`) or raw
449- Generated rules live in `@layer htmlang` (after a `hl-reset` layer), so any
450 CSS in `@style` or `@raw` overrides them.
457 .note { color: gray; }
458@el [animation fade-in 0.3s ease, class=note] Fades in
461## Upgrading older files
463`htmlang upgrade [dir|file]` rewrites removed syntax; the compiler reports each
464removed form with its replacement.
466| Removed | Use instead |
468| `@fn`, `@define`, `@mixin`, `@component` | `@let` (function, bundle; `@style` in a function body is scoped) |
469| `@unless COND` | `@if not COND` |
470| `@for $i in A..B`, `@repeat N` | `@each $i in A..B`, `@each $_ in 1..N` |
471| `grid` (attribute) | `@grid`, or `display grid` |
472| `@switch`, `@match` | `@if $x == a` / `@else if` / `@else` |
473| `@use "file" names`, `@import file` | `@include file` |
474| `@collection`, `@env`, `@translations`, `@fetch` | `@data $name SOURCE` (glob, `env:NAME`, or a JSON file per locale) |
475| `@theme` | `@let --name value` lines |
476| `@json-ld`, `@font-face`, `@manifest` | Raw HTML in `@head`, CSS in `@style` |
477| `@breakpoint`, `@deprecated` | A media query in `@style`; nothing |
478| `@svg [attrs] file.svg` | `@image [inline, attrs] file.svg` |
479| `gap-x`, `gap-y`, `shadow`, `blur N`, `truncate`, `critical` | `column-gap`, `row-gap`, `box-shadow`, `filter blur(N)`, `$truncate`, nothing |
480| `@with $x as y` | `@let y $x` |
481| `@extends layout.hl`, `@layout file` | `@include layout.hl` and a call to the layout, now a function (both files are converted) |
482| `@scope`, `@starting-style`, `@css-property` | The CSS rule in `@style` |
483| `@lang`, `@favicon`, `@canonical`, `@base` | `@page [lang ..., favicon ..., canonical ..., base ...] Title` |
484| `@og KEY VALUE` | `@meta og:KEY VALUE` |
485| `@debug`, `@log` | `@warn` |
486| `@defer` | Its content, directly |
487| `@col`, `@p`, `@img`, `@li`, `@btn`, `@ul`, `@divider`, `@opt` | `@column`, `@paragraph`, `@image`, `@item`, `@button`, `@list`, `@hr`, `@option` |
488| `type email`, `id main` (HTML attributes written as styles) | `type=email`, `id=main` |
489| `skeleton`, `no-scrollbar`, `gradient A B` | `$skeleton`, `$no-scrollbar`, `background linear-gradient(A, B)` |
490| `@tooltip TEXT` | `@tooltip [tip TEXT] TEXT` |
491| `@let x $a + 1` (computed without `=`) | `@let x = $a + 1` |
492| `COND ? A : B` | `if(COND, A, B)` |
493| `key if COND` (in attributes) | `if(COND, key)` |
494| `$a ~ " " ~ $b` | `"$a $b"` |
495| `@each $x in LIST [page N]` | Split the list, or filter it with `@if` |
496| `...$bundle` | `$bundle` |
497| `animate`, `inset-area` | `animation`, `position-area` |
498| `$x\|uppercase`, `$c\|darken:10` (filters) | `${uppercase($x)}`, `${darken($c, 10)}` |
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list