Overview

html`…` is a tagged template. It returns a TemplateResult — not a string and not a DOM node. The first time a component shows it, the DOM is stamped from that template. Later updates patch only the expressions that changed. There is no virtual DOM.

Import html, svg, nothing, noChange, and the lit-html directives (when, repeat, classMap, and the rest) from mates. A component’s inner function returns html. Nest another component with ${x(Child, props)}.

Coming from JSX: expressions use ${…}, events use DOM names with an @ prefix, boolean attributes need ?, and a child value of false becomes the text "false". Use nothing or when to hide a branch.

From JSX to html``

JSX
html``
Notes
{name} ${name} Any JavaScript expression. Text is escaped.
className / htmlFor class / for Real HTML attribute names.
onClick={fn} @click=${fn} DOM event names — @input, not @onChange.
style={{ color }} style="color: ${color}" A CSS string in the template. For a style object, use the style() directive.
disabled={bool} ?disabled=${bool} A bare attribute binding stringifies the boolean. ? toggles presence.
{cond && } ${cond ? html`…` : nothing} false as a child prints the text “false”.
<>…</> One parent, or an array No fragments. Non-void tags need a real closing tag — <div /> is not empty.
<Card /> / spread ${x(Card, props)} No component tags and no prop spread. Pass props to x().
{/* comment */} <!-- comment --> HTML comments inside the template.
SVG inside JSX svg`…` Use svg, also exported from mates, when the SVG namespace matters.

Text, properties, events, boolean attrs

Use ${…} for text, .value / .checked for DOM properties, @event for listeners, and ?attr for boolean attributes. Only changed expressions are patched.

import { html, atom, renderApp } from 'mates';

const App = () => {
const name = atom('Ada');
const color = atom('#00b3a8');
const locked = atom(false);

return () => html`
<div class="m-col m-gap">
<h2 class="m-0" style="color:${color()}">Hello, ${name()}!</h2>
<input
.value=${name()}
@input=${(e) => name.set(e.target.value)}
?disabled=${locked()}
placeholder="Name"
/>
<input
type="color"
.value=${color()}
@input=${(e) => color.set(e.target.value)}
?disabled=${locked()}
/>
<label class="m-flex m-items-center m-gap-sm">
<input
type="checkbox"
.checked=${locked()}
@change=${(e) => locked.set(e.target.checked)}
/>
Lock inputs
</label>
</div>
`;
};
renderApp(App, document.getElementById('app'));

Hide a branch with nothing

nothing clears a child part. Prefer it over false or an empty string when a branch should disappear.

import { html, atom, nothing, renderApp } from 'mates';

const App = () => {
const show = atom(true);
return () => html`
<div class="m-col m-gap">
<button @click=${() => show.set((v) => !v)}>
${show() ? 'Hide' : 'Show'}
</button>
${show() ? html`<p class="m-0">Visible branch</p>` : nothing}
</div>
`;
};
renderApp(App, document.getElementById('app'));

when() for a conditional

when(condition, trueFn, falseFn) runs only the factory for the active branch. Import it from mates.

import { html, atom, nothing, when, renderApp } from 'mates';

const App = () => {
const on = atom(true);
return () => html`
<div class="m-col m-gap">
<button @click=${() => on.set((v) => !v)}>Toggle with when()</button>
${when(
on(),
() => html`<p class="m-0">Branch A</p>`,
() => nothing,
)}
</div>
`;
};
renderApp(App, document.getElementById('app'));