MX
MX is a template language born from Marko: one markup language, many hosts.
It compiles today for HTML, Astro, React, Preact, Hono, SolidMX, Angular and data.
Every construct MX ships, in one file
Hover or focus a marked region to light it up; click, or press Enter, to pin its note. This exact file compiles on the html target on every docs build.
import ProductCard from "./tags/product-card.mx";
import { money } from "./format.ts";
import type { Product } from "./product.ts";
// Concise mode: a tag on its own line without angle brackets, children
// nested under it by indentation, a `--` text line for literal text, and no
// closing tag anywhere in the region.
section.printed
h3
-- Concise mode
p
-- One tag per line, children indented, no closing tags;
-- a text line starts with two hyphens.
export interface Input {
products: Product[];
currency: string;
filter?: string;
emptyHtml?: string;
note?: string;
}
static const MAX_ITEMS = 3;
// A value derived on each render. `<const>` is not a signal.
<const/shown=input.products.slice(0, MAX_ITEMS)/>
<const/cardAttrs={ currency: input.currency }/>
// `<let>` is state: a reactive host re-renders on assignment, the html target
// reads the initial value once.
<let/selectedId=shown[0]?.id/>
<define/Row|label, value|>
<dt>${label}</dt>
<dd>${value}</dd>
</define>
<!-- A source comment: it never reaches the output. -->
<h1#store-title.store-title>${shown.length} products, one file</h1>
<form.search role="search">
<input#q.q name="q" type="search" placeholder="Filter" required/>
</form>
<if=shown.length === 0>
<p.empty>$!{input.emptyHtml ?? "No products yet."}</p>
</if>
<else if=input.filter>
<p.hint>Filtered by ${input.filter}.</p>
</else>
<else>
<p.hint>Showing all ${shown.length} products.</p>
</else>
<ul.store aria-live="polite">
<for|product, i| of=shown>
<li class="row" data-high-value=(product.price > 30) style={ "padding-left": `${i}rem` }>
<ProductCard product=product featured=(i === 0) ...cardAttrs>
<@badge><span.badge>${i === 0 ? "Featured" : "New"}</span></@badge>
<@price|amount|><b.sale>${money(amount, input.currency)}</b></@price>
</ProductCard>
</li>
</for>
</ul>
<Cheapest/best products=shown/>
<dl.summary>
<Row label="Currency" value=input.currency/>
<Row label="Cheapest" value=best ? money(best.price, input.currency) : "—"/>
<Row label="Selected" value=selectedId ?? "none"/>
</dl>
<p.printed>$!{input.note ?? "Printed from one <code>.mx</code> file."}</p>- importA `.mx` file compiles to a template, so import a `.mx` file itself to call it as a tag.Read the docs →
- import typeHelpers and types come from a `.ts` module; a `.mx` file is imported to call it as a tag.Read the docs →
- concise modeConcise mode: a tag on its own line without angle brackets, children nested under it by indentation, a `--` text line for literal text.Read the docs →
- export interface InputThe exported `Input` is the tag's public type: what a call site may pass.Read the docs →
- static`static` runs once at module load, not once per render.Read the docs →
- <const>`<const>` binds a value derived on each render; it is not a signal.Read the docs →
- <let>`<let>` is state. On the html target it is the initial value, read once.Read the docs →
- <define>`<define>` names a fragment you can call like any other tag.Read the docs →
- HTML commentA literal `<!-- -->` is source only: comments are stripped at compile time, so this never reaches the page. `<html-comment>` is the tag that emits one.Read the docs →
- #id and .class shorthand`#id` sets `id` and `.class` adds to `class`, right where they are written.Read the docs →
- escaped `${}``${}` escapes: the value becomes text, never markup.Read the docs →
- boolean attributeA valueless attribute is a boolean attribute, inherited from Marko; this one is `required`.Read the docs →
- expression attribute`name=expr` takes any expression, and a bare `>` would end the tag — so a comparison goes in parentheses.Read the docs →
- spread attribute`...attrs` spreads an object into the call; an attribute written after it wins.Read the docs →
- static attribute`name="value"` is an ordinary static attribute.Read the docs →
- structured style`style` takes a structured value, and its keys are written as CSS properties: `padding-left`, not `paddingLeft`.Read the docs →
- <if> / <else if> / <else>`<if>`, `<else if>` and `<else>` are one chain, each with its own closing tag.Read the docs →
- <for>`<for|item, i| of=list>` iterates; the pipes are tag params.Read the docs →
- custom tagMX collects `tags/` directories as it walks toward the nearest `package.json`; a template there is callable as a tag.Read the docs →
- <@attributeTag>`<@name>` becomes a named prop of the call instead of part of its children.Read the docs →
- attribute tag paramsPipes on an attribute tag make that prop a function of its parameters.Read the docs →
- tag variable`/name` binds the value the callee hands back with `<return>` — `undefined` here, because the list can be empty.Read the docs →
- raw `$!{}``$!{}` interpolates raw: the value is inserted as markup, unescaped.Read the docs →