MX

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.

Read the introductionThe specification

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>
  1. importA `.mx` file compiles to a template, so import a `.mx` file itself to call it as a tag.Read the docs →
  2. import typeHelpers and types come from a `.ts` module; a `.mx` file is imported to call it as a tag.Read the docs →
  3. 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 →
  4. export interface InputThe exported `Input` is the tag's public type: what a call site may pass.Read the docs →
  5. static`static` runs once at module load, not once per render.Read the docs →
  6. <const>`<const>` binds a value derived on each render; it is not a signal.Read the docs →
  7. <let>`<let>` is state. On the html target it is the initial value, read once.Read the docs →
  8. <define>`<define>` names a fragment you can call like any other tag.Read the docs →
  9. 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 →
  10. #id and .class shorthand`#id` sets `id` and `.class` adds to `class`, right where they are written.Read the docs →
  11. escaped `${}``${}` escapes: the value becomes text, never markup.Read the docs →
  12. boolean attributeA valueless attribute is a boolean attribute, inherited from Marko; this one is `required`.Read the docs →
  13. expression attribute`name=expr` takes any expression, and a bare `>` would end the tag — so a comparison goes in parentheses.Read the docs →
  14. spread attribute`...attrs` spreads an object into the call; an attribute written after it wins.Read the docs →
  15. static attribute`name="value"` is an ordinary static attribute.Read the docs →
  16. structured style`style` takes a structured value, and its keys are written as CSS properties: `padding-left`, not `paddingLeft`.Read the docs →
  17. <if> / <else if> / <else>`<if>`, `<else if>` and `<else>` are one chain, each with its own closing tag.Read the docs →
  18. <for>`<for|item, i| of=list>` iterates; the pipes are tag params.Read the docs →
  19. custom tagMX collects `tags/` directories as it walks toward the nearest `package.json`; a template there is callable as a tag.Read the docs →
  20. <@attributeTag>`<@name>` becomes a named prop of the call instead of part of its children.Read the docs →
  21. attribute tag paramsPipes on an attribute tag make that prop a function of its parameters.Read the docs →
  22. tag variable`/name` binds the value the callee hands back with `<return>` — `undefined` here, because the list can be empty.Read the docs →
  23. raw `$!{}``$!{}` interpolates raw: the value is inserted as markup, unescaped.Read the docs →