on master help sign in
htmlang/examples/demo.hl
303 lines 14 kB history raw
1-- A one-page tour of htmlang. Each section shows one part of the language:
2-- layout, text, CSS styles, functions, expressions, control flow and HTML.
3@page [
4 lang=en,
5 color var(--ink),
6 background var(--page),
7 line-height 1.5,
8 -- A custom property is a style like any other: dark mode redefines the
9 -- tokens here, once, and everything that uses them follows
10 dark:[
11 --ink #e5e7eb,
12 --muted #94a3b8,
13 --line #1f2937,
14 --subtle #1e293b,
15 --page #030712,
16 --surface #111827
17 ]
18] htmlang tour
19@meta description A one-page tour of htmlang.
20@meta og:title htmlang tour
22-- Values
23@let --brand #3b82f6
24@let --ink #0f172a
25@let --muted #64748b
26@let --line #e5e7eb
27@let --subtle #f8fafc
28@let --page white
29@let --surface white
31-- Computed values: `=` makes the rest an expression
32@let unit 8
33@let gap = $unit * 2
34@let gutter = $gap + $unit
36-- Attribute bundles
37@let card [
38 padding $gap,
39 color var(--ink),
40 background var(--surface),
41 border 1 solid var(--line),
42 border-radius $unit
44@let label [font-size 12, font-weight 700, color var(--muted), text-transform uppercase, letter-spacing 1]
46-- A section of the tour: a function with a parameter, a default, and children
47@let @chapter [title, number ""]
48 @section [spacing $gap, padding-block $gutter, border-top 1 solid var(--line)]
49 @row [spacing $unit, align-items baseline]
50 @if $number
51 @text [color var(--brand), font-weight 700] $number.
52 @h2 [font-size 26] $title
53 @children
55-- Plain CSS: keyframes, and anything else a stylesheet can hold
56@style
57 @keyframes rise {
58 from { opacity: 0; transform: translateY(6px); }
59 to { opacity: 1; transform: none; }
60 }
62@el [width fill, max-width 960, center-x, padding $gutter]
63 @header [spacing $unit, padding-block $gutter, animation rise 0.4s ease]
64 @text [$label] A one-page tour
65 @h1 [font-size 44, line-height 1.1, letter-spacing -1] Welcome to htmlang
66 @paragraph [font-size 18, color var(--muted), max-width 620]
67 A small layout language that compiles to
68 {@text [font-weight 700, color var(--brand)] one static HTML file}.
69 Structure starts with {@code @}, everything else is content.
71 -- 1. Layout ---------------------------------------------------------------
72 @chapter [title Layout, number 1]
73 @paragraph
74 {@code @el} stacks its children in a column and {@code @row} lays them
75 out side by side. {@code spacing} is the gap between children.
76 {@code flex-direction row} makes any column a row, also from a width
77 up ({@code md:flex-direction row}), and the children's
78 {@code width fill} follows it.
79 @row [spacing $unit, wrap]
80 @el [$card, width fill] width fill
81 @el [$card, width fill] shares the row
82 @el [$card, width 160] width 160
83 @row [$card, spacing $unit, align-items center]
84 @text [font-weight 700] Logo
85 @spacer
86 @text @spacer pushes this to the end
87 @el [spacing $unit, md:flex-direction row]
88 @el [$card, width fill] Stacked on a phone
89 @el [$card, width fill] side by side from md up
90 @row [spacing $unit, height 120]
91 @el [$card, align-top] align-top
92 @el [$card, center-y] center-y
93 @el [$card, align-bottom] align-bottom
94 @grid [grid-cols 4, spacing $unit]
95 @each $n in 1..6
96 @el [$card, text-align center, if($n == 1, col-span 2)] Cell $n
97 @el [$card, height 120, overflow hidden]
98 @behind
99 @el [background linear-gradient(135deg, #dbeafe, #fae8ff), dark:background linear-gradient(135deg, #1e3a8a, #581c87), height fill, justify-content flex-end, padding $unit] (behind)
100 @text [font-weight 700] @in-front and @behind layer content
101 @in-front
102 @text [align-right, align-bottom, padding $unit, font-size 12, color var(--muted)] painted on top
104 -- 2. Text ----------------------------------------------------------------
105 @chapter [title Text, number 2]
106 @paragraph [max-width 640]
107 Lines without an {@code @} are text. Braces put elements inside a line:
108 {@b bold}, {@i italic}, {@strong important}, H{@sub 2}O,
109 {@mark highlighted}, {@link [color var(--brand), text-decoration underline] https://example.com a link},
110 and {@kbd [padding 1 6, border 1 solid var(--line), border-radius 4] Ctrl+K}.
111 @paragraph
112 A backslash escapes: \@ at the start of a line, \$price, and \{braces}.
113 -- `>` chains elements that have one child each
114 @el [padding 12 20, background var(--brand), border-radius 8, align-left] > @link /start
115 @text [color white, font-weight 700] Get started
116 @blockquote [padding-inline-start $gap, border-left 3 solid var(--brand), color var(--muted), font-style italic]
117 Layout is code. Style is code. Everything should compose.
118 @cite — Anonymous
120 -- 3. Style ---------------------------------------------------------------
121 @chapter [title Style, number 3]
122 @paragraph
123 Besides the layout attributes, every attribute is a CSS property with its
124 CSS name and value. A bare number in a length, shadows included, is pixels, and \\, keeps a comma in a value.
125 A custom property (--name) is one too: set on an element, it holds for everything inside it.
126 Prefixes stack, and one prefix before a [group] applies to each style in it.
127 A selector prefix is a CSS pseudo-class or pseudo-element under its own name: hover:, first-child:, nth-child(odd):, has(...):, marker:.
128 An element prefix styles every such element inside, at any depth: the table in chapter 6 pads all its cells with {@code @td:padding 8}.
129 @row [spacing $unit, wrap]
130 @el [$card, box-shadow 0 1 3 rgba(0,0,0,0.1)\, 0 8 24 rgba(0,0,0,0.08), transform rotate(-2deg)] Any CSS property
131 @el [$card, hover:background #eff6ff, hover:border-color var(--brand), transition all 0.15s ease, cursor pointer] hover:
132 @el [$card, md:[background #ecfdf5, border-color #10b981], dark:[background #064e3b, hover:background #065f46]] md:[...] and dark:hover:
133 @el [$card, --tone #a855f7, color var(--tone), border-color var(--tone)] custom properties
134 @el [$card, before:content "→ ", before:color var(--brand)] before:
135 @ul [$card, spacing 4, list-style disc, padding-inline-start 36, children:display list-item, children:marker:color var(--brand), children:padding 4 8]
136 @each $item in first-child: bolds this one, nth-child(odd): shades every other row, children: pads them all, children:marker: colours the bullets
137 @li [first-child:font-weight 700, nth-child(odd):background var(--subtle), border-radius 4] $item
138 @el [$card, has(input:checked):[border-color var(--brand), background #eff6ff]]
139 @label [cursor pointer]
140 @input [type=checkbox, margin-inline-end 8]
141 Tick the box: has() styles the card around it
142 @paragraph
143 Colour maths is CSS too, so a variant follows its token wherever it is redefined:
144 @row [spacing $unit, wrap]
145 @each $shade in 0, 10, 20, 30
146 @el [padding 16, border-radius $unit, color white, background color-mix(in srgb, var(--brand), black $shade%)]
147 color-mix … black $shade%
148 @el [padding 16, border-radius $unit, background rgb(from var(--brand) r g b / 0.2)] rgb(from …) / 0.2
149 @el [padding 16, border-radius $unit, background color-mix(in srgb, var(--brand), #f43f5e 50%)] color-mix … 50%
151 -- 4. Functions -----------------------------------------------------------
152 @let @stat [value, caption, tone var(--brand)]
153 @el [$card, spacing 4, flex 1 1 160px]
154 @text [font-size 32, font-weight 800, color $tone] $value
155 @text [color var(--muted)] $caption
157 @let @panel [title]
158 @article [$card, spacing $unit]
159 @row [align-items center]
160 @h3 [font-size 18] $title
161 @spacer
162 @slot actions
163 @children
164 @slot footer
165 @text [font-size 13, color var(--muted)] Default footer
167 @let @badge [text, dot false]
168 @style
169 & { animation: rise 0.3s ease; }
170 .dot { color: var(--brand); }
171 @row [spacing 6, padding 2 10, border-radius 999, background var(--subtle), font-size 13]
172 @if $dot
173 @text [class=dot] •
174 $text
176 @let @pitch
177 Functions are {@code @let @name} with a body. Their
178 parameters are listed and passed like attributes, a name alone passes
179 true, and a body can be text like this one.
181 @let @key
182 @kbd [padding 1 6, border 1 solid var(--line), border-radius 4, font-size 13]
183 @children
185 @chapter [title Functions, number 4]
186 @paragraph
187 @pitch
188 @paragraph
189 A call works wherever an element does, even inside a sentence:
190 press {@key Ctrl} {@key K} to search.
191 @row [spacing $unit, wrap]
192 @stat [value 1, caption output file]
193 @stat [value 0, caption lines of JavaScript, tone #10b981]
194 @stat [value ∞, caption components, tone #a855f7, background var(--subtle)]
195 @panel [title Named slots]
196 @slot actions
197 @link [color var(--brand), font-size 14] /edit Edit
198 @paragraph
199 Children fill {@code @children}; {@code @slot} blocks fill slots by name,
200 and a slot name the panel doesn't have is an error.
201 @row [spacing $unit]
202 @badge [text scoped style, dot]
203 @badge [text same function]
205 -- 5. Expressions ---------------------------------------------------------
206 @let name htmlang
207 @data $tags ["layout", "css", "static"]
208 @let compact false
209 @chapter [title Expressions, number 5]
210 @paragraph
211 \$name inserts a value and \${...} any expression: ${uppercase($name)}
212 has ${length($name)} letters and ${length($tags)} tags.
213 @row [spacing $unit, wrap]
214 @each $tag in $tags
215 @el [padding ${if($compact, 4, 8)} 12, border-radius $unit, background var(--subtle)]
216 ${capitalize($tag)}: ${length($tag)} letters
217 @if contains($tags, css)
218 @text [color var(--muted)] contains(\$tags, css) is true
220 -- 6. Control flow --------------------------------------------------------
221 @data $team [
222 {"name": "Ada", "role": "Engineering", "active": true},
223 {"name": "Grace", "role": "Design, research", "active": true},
224 {"name": "Linus", "role": "Operations", "active": false}
225 ]
226 @data $nobody []
227 @chapter [title Control flow, number 6]
228 @table [border-collapse collapse, width 100%, @th:padding 8, @td:padding 8]
229 @thead
230 @tr [text-align left, color var(--muted), font-size 13]
231 @th #
232 @th Name
233 @th Role
234 @th Status
235 @tbody
236 @each $person, $i in $team
237 @tr [border-top 1 solid var(--line)]
238 @td ${$i + 1}
239 @td [font-weight 600] $person.name
240 @td $person.role
241 @td [if($person.active, color #10b981, color var(--muted))]
242 @if $person.active
243 Active
244 @else
245 Away
246 @row [spacing 4]
247 @each $n in 10..0 step 2
248 @text [$card, padding 4 10] $n
249 @each $x in $nobody
250 @text $x
251 @else
252 @text [color var(--muted), font-style italic] @each … @else shows this for an empty list.
254 -- 7. HTML ----------------------------------------------------------------
255 @chapter [title HTML, number 7]
256 @paragraph
257 Every other element has its HTML name, and HTML attributes are written
258 {@code key=value}. {@code @page} is the root element: its styles go on
259 {@code <body>}, a column that fills the window, and its {@code key=value}
260 attributes on {@code <html>}, like this page's {@code lang=en}.
261 @row [spacing $gutter, wrap, align-items flex-start]
262 @form [$card, spacing $unit, flex 1 1 280px, method=post] /subscribe
263 @label [for=email, font-weight 600] Email
264 @input [
265 id=email,
266 type=email,
267 name=email,
268 placeholder=you@example.com,
269 required,
270 padding 10,
271 border 1 solid var(--line),
272 border-radius 6,
273 focus:outline 2 solid var(--brand),
274 invalid:border-color #ef4444
275 ]
276 @label [for=topic, font-weight 600] Topic
277 @select [id=topic, name=topic, padding 10, border-radius 6]
278 @option [value=news] News
279 @option [value=releases] Releases
280 @button [type=submit, padding 10, border 0, border-radius 6, background var(--brand), color white, font-weight 700, cursor pointer, hover:background color-mix(in srgb, var(--brand), black 10%)] Subscribe
281 @el [spacing $unit, flex 1 1 280px]
282 @details [$card]
283 @summary [font-weight 600, cursor pointer] What does this compile to?
284 @paragraph [color var(--muted), padding-block-start 8]
285 Flexbox and grid CSS in one {@code <style>}, and no JavaScript.
286 @button [type=button, popovertarget=about, $card, cursor pointer] Open a popover
287 @el [popover, id=about, $card, max-width 320]
288 Native HTML popovers need no script.
289 @row [spacing $unit, align-items center, font-size 14, color var(--muted)]
290 @text Progress
291 @progress [value=60, max=100]
292 @time [datetime=2026-09-26] Sept 26
293 @figure [spacing 4]
294 @image [alt=A placeholder landscape, border-radius $unit, width 100%] https://picsum.photos/640/240
295 @figcaption [font-size 13, color var(--muted)] @figure and @figcaption
297 -- Raw HTML is the escape hatch
298 @raw <hr style="width: 100%; border: none; border-top: 1px dashed #cbd5e1">
300 @footer [flex-direction row, spacing $unit, padding-block $gutter, color var(--muted), font-size 14, print:display none]
301 @text © 2026 htmlang
302 @spacer
303 @link [hover:color var(--brand)] https://github.com/iko-soy/htmlang Source