-- A one-page tour of htmlang. Each section shows one part of the language: -- layout, text, CSS styles, functions, expressions, control flow and HTML. @page [ lang=en, color var(--ink), background var(--page), line-height 1.5, -- A custom property is a style like any other: dark mode redefines the -- tokens here, once, and everything that uses them follows dark:[ --ink #e5e7eb, --muted #94a3b8, --line #1f2937, --subtle #1e293b, --page #030712, --surface #111827 ] ] htmlang tour @meta description A one-page tour of htmlang. @meta og:title htmlang tour -- Values @let --brand #3b82f6 @let --ink #0f172a @let --muted #64748b @let --line #e5e7eb @let --subtle #f8fafc @let --page white @let --surface white -- Computed values: `=` makes the rest an expression @let unit 8 @let gap = $unit * 2 @let gutter = $gap + $unit -- Attribute bundles @let card [ padding $gap, color var(--ink), background var(--surface), border 1 solid var(--line), border-radius $unit ] @let label [font-size 12, font-weight 700, color var(--muted), text-transform uppercase, letter-spacing 1] -- A section of the tour: a function with a parameter, a default, and children @let @chapter [title, number ""] @section [spacing $gap, padding-block $gutter, border-top 1 solid var(--line)] @row [spacing $unit, align-items baseline] @if $number @text [color var(--brand), font-weight 700] $number. @h2 [font-size 26] $title @children -- Plain CSS: keyframes, and anything else a stylesheet can hold @style @keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } @el [width fill, max-width 960, center-x, padding $gutter] @header [spacing $unit, padding-block $gutter, animation rise 0.4s ease] @text [$label] A one-page tour @h1 [font-size 44, line-height 1.1, letter-spacing -1] Welcome to htmlang @paragraph [font-size 18, color var(--muted), max-width 620] A small layout language that compiles to {@text [font-weight 700, color var(--brand)] one static HTML file}. Structure starts with {@code @}, everything else is content. -- 1. Layout --------------------------------------------------------------- @chapter [title Layout, number 1] @paragraph {@code @el} stacks its children in a column and {@code @row} lays them out side by side. {@code spacing} is the gap between children. {@code flex-direction row} makes any column a row, also from a width up ({@code md:flex-direction row}), and the children's {@code width fill} follows it. @row [spacing $unit, wrap] @el [$card, width fill] width fill @el [$card, width fill] shares the row @el [$card, width 160] width 160 @row [$card, spacing $unit, align-items center] @text [font-weight 700] Logo @spacer @text @spacer pushes this to the end @el [spacing $unit, md:flex-direction row] @el [$card, width fill] Stacked on a phone @el [$card, width fill] side by side from md up @row [spacing $unit, height 120] @el [$card, align-top] align-top @el [$card, center-y] center-y @el [$card, align-bottom] align-bottom @grid [grid-cols 4, spacing $unit] @each $n in 1..6 @el [$card, text-align center, if($n == 1, col-span 2)] Cell $n @el [$card, height 120, overflow hidden] @behind @el [background linear-gradient(135deg, #dbeafe, #fae8ff), dark:background linear-gradient(135deg, #1e3a8a, #581c87), height fill, justify-content flex-end, padding $unit] (behind) @text [font-weight 700] @in-front and @behind layer content @in-front @text [align-right, align-bottom, padding $unit, font-size 12, color var(--muted)] painted on top -- 2. Text ---------------------------------------------------------------- @chapter [title Text, number 2] @paragraph [max-width 640] Lines without an {@code @} are text. Braces put elements inside a line: {@b bold}, {@i italic}, {@strong important}, H{@sub 2}O, {@mark highlighted}, {@link [color var(--brand), text-decoration underline] https://example.com a link}, and {@kbd [padding 1 6, border 1 solid var(--line), border-radius 4] Ctrl+K}. @paragraph A backslash escapes: \@ at the start of a line, \$price, and \{braces}. -- `>` chains elements that have one child each @el [padding 12 20, background var(--brand), border-radius 8, align-left] > @link /start @text [color white, font-weight 700] Get started @blockquote [padding-inline-start $gap, border-left 3 solid var(--brand), color var(--muted), font-style italic] Layout is code. Style is code. Everything should compose. @cite — Anonymous -- 3. Style --------------------------------------------------------------- @chapter [title Style, number 3] @paragraph Besides the layout attributes, every attribute is a CSS property with its CSS name and value. A bare number in a length, shadows included, is pixels, and \\, keeps a comma in a value. A custom property (--name) is one too: set on an element, it holds for everything inside it. Prefixes stack, and one prefix before a [group] applies to each style in it. A selector prefix is a CSS pseudo-class or pseudo-element under its own name: hover:, first-child:, nth-child(odd):, has(...):, marker:. 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}. @row [spacing $unit, wrap] @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 @el [$card, hover:background #eff6ff, hover:border-color var(--brand), transition all 0.15s ease, cursor pointer] hover: @el [$card, md:[background #ecfdf5, border-color #10b981], dark:[background #064e3b, hover:background #065f46]] md:[...] and dark:hover: @el [$card, --tone #a855f7, color var(--tone), border-color var(--tone)] custom properties @el [$card, before:content "→ ", before:color var(--brand)] before: @ul [$card, spacing 4, list-style disc, padding-inline-start 36, children:display list-item, children:marker:color var(--brand), children:padding 4 8] @each $item in first-child: bolds this one, nth-child(odd): shades every other row, children: pads them all, children:marker: colours the bullets @li [first-child:font-weight 700, nth-child(odd):background var(--subtle), border-radius 4] $item @el [$card, has(input:checked):[border-color var(--brand), background #eff6ff]] @label [cursor pointer] @input [type=checkbox, margin-inline-end 8] Tick the box: has() styles the card around it @paragraph Colour maths is CSS too, so a variant follows its token wherever it is redefined: @row [spacing $unit, wrap] @each $shade in 0, 10, 20, 30 @el [padding 16, border-radius $unit, color white, background color-mix(in srgb, var(--brand), black $shade%)] color-mix … black $shade% @el [padding 16, border-radius $unit, background rgb(from var(--brand) r g b / 0.2)] rgb(from …) / 0.2 @el [padding 16, border-radius $unit, background color-mix(in srgb, var(--brand), #f43f5e 50%)] color-mix … 50% -- 4. Functions ----------------------------------------------------------- @let @stat [value, caption, tone var(--brand)] @el [$card, spacing 4, flex 1 1 160px] @text [font-size 32, font-weight 800, color $tone] $value @text [color var(--muted)] $caption @let @panel [title] @article [$card, spacing $unit] @row [align-items center] @h3 [font-size 18] $title @spacer @slot actions @children @slot footer @text [font-size 13, color var(--muted)] Default footer @let @badge [text, dot false] @style & { animation: rise 0.3s ease; } .dot { color: var(--brand); } @row [spacing 6, padding 2 10, border-radius 999, background var(--subtle), font-size 13] @if $dot @text [class=dot] • $text @let @pitch Functions are {@code @let @name} with a body. Their parameters are listed and passed like attributes, a name alone passes true, and a body can be text like this one. @let @key @kbd [padding 1 6, border 1 solid var(--line), border-radius 4, font-size 13] @children @chapter [title Functions, number 4] @paragraph @pitch @paragraph A call works wherever an element does, even inside a sentence: press {@key Ctrl} {@key K} to search. @row [spacing $unit, wrap] @stat [value 1, caption output file] @stat [value 0, caption lines of JavaScript, tone #10b981] @stat [value ∞, caption components, tone #a855f7, background var(--subtle)] @panel [title Named slots] @slot actions @link [color var(--brand), font-size 14] /edit Edit @paragraph Children fill {@code @children}; {@code @slot} blocks fill slots by name, and a slot name the panel doesn't have is an error. @row [spacing $unit] @badge [text scoped style, dot] @badge [text same function] -- 5. Expressions --------------------------------------------------------- @let name htmlang @data $tags ["layout", "css", "static"] @let compact false @chapter [title Expressions, number 5] @paragraph \$name inserts a value and \${...} any expression: ${uppercase($name)} has ${length($name)} letters and ${length($tags)} tags. @row [spacing $unit, wrap] @each $tag in $tags @el [padding ${if($compact, 4, 8)} 12, border-radius $unit, background var(--subtle)] ${capitalize($tag)}: ${length($tag)} letters @if contains($tags, css) @text [color var(--muted)] contains(\$tags, css) is true -- 6. Control flow -------------------------------------------------------- @data $team [ {"name": "Ada", "role": "Engineering", "active": true}, {"name": "Grace", "role": "Design, research", "active": true}, {"name": "Linus", "role": "Operations", "active": false} ] @data $nobody [] @chapter [title Control flow, number 6] @table [border-collapse collapse, width 100%, @th:padding 8, @td:padding 8] @thead @tr [text-align left, color var(--muted), font-size 13] @th # @th Name @th Role @th Status @tbody @each $person, $i in $team @tr [border-top 1 solid var(--line)] @td ${$i + 1} @td [font-weight 600] $person.name @td $person.role @td [if($person.active, color #10b981, color var(--muted))] @if $person.active Active @else Away @row [spacing 4] @each $n in 10..0 step 2 @text [$card, padding 4 10] $n @each $x in $nobody @text $x @else @text [color var(--muted), font-style italic] @each … @else shows this for an empty list. -- 7. HTML ---------------------------------------------------------------- @chapter [title HTML, number 7] @paragraph Every other element has its HTML name, and HTML attributes are written {@code key=value}. {@code @page} is the root element: its styles go on {@code }, a column that fills the window, and its {@code key=value} attributes on {@code }, like this page's {@code lang=en}. @row [spacing $gutter, wrap, align-items flex-start] @form [$card, spacing $unit, flex 1 1 280px, method=post] /subscribe @label [for=email, font-weight 600] Email @input [ id=email, type=email, name=email, placeholder=you@example.com, required, padding 10, border 1 solid var(--line), border-radius 6, focus:outline 2 solid var(--brand), invalid:border-color #ef4444 ] @label [for=topic, font-weight 600] Topic @select [id=topic, name=topic, padding 10, border-radius 6] @option [value=news] News @option [value=releases] Releases @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 @el [spacing $unit, flex 1 1 280px] @details [$card] @summary [font-weight 600, cursor pointer] What does this compile to? @paragraph [color var(--muted), padding-block-start 8] Flexbox and grid CSS in one {@code