Overview

Mates templates are lit-html tagged templates: html`…` returns a TemplateResult. On re-render, only the expressions that changed are patched in the live DOM — there is no virtual DOM tree.

Import html, nothing, and re-exported directives from mates. You do not add lit-html as a separate app dependency.

Binding prefixes

Syntax
Sets
Notes
${expr} Child / text Text, nested TemplateResult, arrays, primitives. Prefer nothing to clear.
attr=${val} HTML attribute Pass null/undefined to remove.
.prop=${val} DOM property Use for .value, .checked, .disabled, etc.
@event=${fn} Event listener Stable across renders — equivalent to addEventListener.
?attr=${bool} Boolean attribute Present when true, removed when false.

Dynamic expressions in html``

Embed any JavaScript expression inside ${...}. Mates re-renders only the parts of the DOM that actually changed. Use .value (dot prefix) for DOM property bindings and @event for event listeners.

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

const App = () => {
const name = atom('World');
const count = atom(0);
const show = atom(true);
const color = atom('#00b3a8');

return () => html`
<div class="m-col m-gap">
<!-- Text interpolation -->
<h2 class="m-0" style="color:${color()}">Hello, ${name()}!</h2>

<!-- Attribute binding -->
<input .value=${name()} @input=${e => name.set(e.target.value)} placeholder="Name" />

<!-- Property binding (.value) vs attribute binding -->
<input type="color" .value=${color()} @input=${e => color.set(e.target.value)} />

<!-- Conditional rendering -->
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button @click=${() => show.set(v => !v)}>${show() ? 'Hide' : 'Show'} counter</button>
</div>
${show()
? html`<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-danger" @click=${() => count.set(n => n - 1)}>−</button>
<h1>${count()}</h1>
<button class="btn-primary" @click=${() => count.set(n => n + 1)}>+</button>
</div>`
: html``
}
</div>
`;
};
renderApp(App, document.getElementById('app'));

Clear a branch with nothing

Prefer nothing over an empty string when hiding a branch — it clears the child part cleanly without leftover nodes.

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'));