html`` templates
Fluixi lets you author markup two ways: JSX, or a tagged html template. Both compile to the *same* fine-grained DOM calls, so there is no runtime difference — html is a JSX-free authoring surface for when you do not want to configure JSX in
your tsconfig.
import { html } from '@fluixi/core';
function Hello(props: { name: string }) {
return html`<h1>Hello ${props.name}</h1>`;
}
Import html (and svg) from @fluixi/core, or from @fluixi/start in a
meta-framework app.
How it compiles
This matters for understanding everything below. The template is not parsed at runtime. At build time the compiler splits it into static markup and holes, emits the static part once, and wires each hole to a targeted DOM call:
html`<p class="row">Count: ${count()}</p>`;
becomes, in effect, one <p class="row"> built once and a single text node kept up to
date by an effect. Nothing re-renders; nothing is diffed.
Two consequences worth knowing:
- A hole's value is never parsed as HTML. It is assigned as text, as an attribute, or
as a property. Interpolating a string containing
<script>inserts that text, it does not create an element. The one deliberate exception ishtml=, which is documented as an escape hatch for exactly that reason. - The template's shape is fixed at build time. A hole can supply a value, a child, a component or a set of props — but not a tag name or an attribute name, because those decide the shape. See dynamic tags for the component case.
Interpolation
Any ${…} is a reactive hole. Reading a signal inside one keeps that spot — and only
that spot — up to date:
const [count, setCount] = createSignal(0);
html`<p>Count: ${count()}</p>`;
Where a hole can go
html`<p>${text()}</p>`; // child — text, node, array, or another template
html`<div title=${label()}></div>`; // whole attribute value
html`<div class="card ${theme()}"></div>`; // part of a quoted value
html`<div ...${attrs}></div>`; // a whole props object
html`<${Card} />`; // a component in tag position
A hole that fills an entire attribute value passes through untouched, so it can be any
type — a number, a boolean, an object for class/style. Quoting makes no difference
there; value=${n} and value="${n}" compile identically.
Mixing static text with a hole is string interpolation, and the result is always a string:
html`<input value=${0} />`; // the number 0
html`<div class="card ${theme()}"></div>`; // the string "card dark"
Reading a signal, not passing it
Call the accessor inside the hole. The hole itself is the reactive boundary, so the compiler wraps it in an effect for you:
html`<p>${count()}</p>`; // ✓ tracked — re-runs this text node only
html`<p>${count}</p>`; // renders the function, not its value
Components
Two forms, and the difference is only about how TypeScript sees the name:
// Dynamic tag — `${Card}` is a real expression, so TypeScript counts it as used.
html`<${Card} title=${t()} />`;
// Static tag — bare name, matching JSX.
html`<Card title=${t()} />`;
The static form needs something to tell TypeScript the name is in use, since it is only
text inside a string: either @fluixi/ts-plugin, or a
resolve rule that supplies the component without an
import. In a plain-TypeScript project with neither, prefer <${Card}/>.
Children and closing tags work as expected. A dynamic tag closes with the same expression:
html`<${Card}>
<p>Body</p>
</${Card}>`;
html`<Card>
<p>Body</p>
</Card>`;
Self-closing works for both forms, and — unlike HTML — for any component.
Auto-imported components
Control-flow and router components resolve without an import: write <Show>, <For>,
<Router>, <Outlet> and the compiler adds it for you.
html`
<${Show} when=${user()} fallback=${html`<a href="/login">Sign in</a>`}>
<p>Welcome, ${user()!.name}</p>
</${Show}>
`;
Your own components and a component library's can join them — see component resolution.
Props
Every prop is a hole, and each is independently reactive:
html`<${Row} label="Static" count=${n()} onSelect=${pick} ...${rest} />`;
Children arrive as props.children. A function child is passed through as a function,
which is what <For> and each rely on.
Nested templates
A hole can hold another template — useful for fallback, list items, or conditional
branches:
html`<ul>${items().map((i) => html`<li>${i.label}</li>`)}</ul>`;
Nested templates compile like any other: the inner one is built once and reused per call.
For a list that changes, prefer each or <For> — .map() rebuilds
every row on every change, while <For> reuses the rows whose data did not move.
SVG
Use the svg tag to put a subtree in the SVG namespace. Elements created there need the
SVG namespace, which the html tag does not apply:
import { svg } from '@fluixi/core';
svg`<circle cx="50" cy="50" r=${r()} />`;
An <svg> root written inside html also works — the compiler recognises the tag and creates the subtree in the SVG namespace. Reach for svg when a template
starts below the root, with a <circle> or <path> that has no <svg> ancestor in the
same template to give it away.
Whitespace and text
Whitespace is preserved as written, like HTML — collapsed by CSS at render time, not by
the compiler. <pre>, <textarea>, <script> and <style> keep their content verbatim.
Editor support
@fluixi/ts-plugin (included in every scaffolded app, and shipped in the VS Code
extension) gives html `` real IntelliSense: hover, completion of element attributes
and component props, go-to-definition on tags, and type-checking of the JavaScript inside
the holes — the same experience JSX gets. Reference it in tsconfig.json:
{ "compilerOptions": { "plugins": [{ "name": "@fluixi/ts-plugin" }] } }
When to reach for html``
- You do not want
jsx/jsxImportSourcein your tsconfig (a library, a script, plain TS). - You prefer template-literal markup.
- You want the
html``-only directives:if/else,each,bind:,class:name,style:prop. - You want to mix the two — a
html`` block inside a JSX component, or vice-versa, both compile.
Next: the full directives reference (events, bindings,
if/each, …).