---
title: "Template Syntax and the html Tag"
description: "The template language is four rules: live states, live expressions, snapshot text, and lists and branches as plain JavaScript. XSS-safe, no compile step."
source: https://lemonadejs.com/docs/templates/
---

# Templates and the html tag

A template is a tagged literal: `` html`...` ``. The tag parses the static
parts **once per call site** (the template strings identity is the cache
key) and packages this call's values; it renders nothing. `mount()`
renders. Everything `${...}` can do follows from four rules:

```javascript
html`<div>
    ${count}                                  <!-- 1. state: live value -->
    ${() => count.value * 2}                  <!-- 2. arrow: live expression -->
    ${props.start}                            <!-- 3. plain: one-time snapshot -->
    ${() => items.value.map(x => html`<li>${x}</li>`)}   <!-- 4. lists/branches -->
</div>`
```

1. **`${state}`** is live. It updates whenever the state changes.
2. **`${() => expr}`** is live. It re-runs when any state it *reads* changes.
3. **`${plainValue}`** is a snapshot, evaluated once. Plain strings are
   always **text, never HTML**.
4. **Lists and conditionals are just expressions** returning views or
   arrays of views: `.map()` and `&&`, not directives.

That is the whole language. `llms.txt` carries it in ten lines.

The four rules, live. The list and the branch are arrow functions; the plain value is a one-time snapshot.

<!--example-->

```js
import { html } from 'lemonadejs';

const App = (props, { state }) => {
    const count = state(1);
    const items = state(['Templates', 'State', 'Events']);
    const start = 1;                                          // a plain value: snapshot

    return html`<div>
        <p>Live state: <b>${count}</b> · live expression: <b>${() => count.value * 2}</b>
           · snapshot: <b>${start}</b></p>
        <button onclick="${() => count.value++}">+1</button>
        <button onclick="${() => (items.value = [...items.value, 'Item ' + (items.value.length + 1)])}">add item</button>
        <ul>${() => items.value.map((x) => html`<li>${x}</li>`)}</ul>
        ${() => count.value > 3 && html`<p><i>Branch: rendered once count passes 3.</i></p>`}
    </div>`;
};
```

## Mechanics

**Control flow is JavaScript.** The engine never knows a loop exists: a
slot value is `false | view | array`, and the engine reconciles whatever
the expression returned:

```javascript
${() => valid.value && html`<div>shown while valid</div>`}
${() => items.value.map(x => html`<li>${x}</li>`)}
```

Inside the inner template, plain snapshots (`${x}`) are correct: the
outer arrow re-runs and produces fresh values.

**Live expressions are auto-tracked per run.** Dependencies re-track on
every evaluation, so conditional reads work, and
`a.value ? b.value : c.value` tracks `b` only while `a` is true.

**Strings are text, never markup.** Slot strings become Text nodes: the
engine writes `createTextNode`/`nodeValue`, never `innerHTML`, so
injection by accident is impossible, *including* in the zero-build
deployment. The explicit opt-out for markup you trust:

```javascript
import { unsafe } from 'lemonadejs';
html`<article>${unsafe(trustedHtmlFromCms)}</article>`   // never on user input
```

**Updates are surgical.** Each text-position slot owns a marker node and
reconciles positionally: same kind of content at the same position keeps
its DOM; a branch that toggles off keeps exactly one hidden generation, so
show/hide costs nothing; identical values (`Object.is` across the slot's
values) are detected before any DOM work. Static siblings are never
revisited; the template itself never re-runs. Lists that reorder add
`key="${...}"` on the item's root for identity-based matching, and the
complete story is [Lists](/docs/lists/).

**Mistakes have codes.** Mismatched and unclosed tags fail at parse with
the tag named (`LJS-101`, `LJS-102`); an expression in an illegal position
(attribute *name*, partial tag name) is `LJS-105`; a snapshot that looks
like it was meant to be live (states were read while the template was
built and a slot holds a primitive) warns `LJS-202` in dev. Literal
`a < b` renders fine; `<!-- comments -->` are dropped, including
expressions inside them; template indentation never reaches the DOM.

**No eval, ever.** The parser is hand-written; the library contains no
`eval` and no `new Function`. Zero-build under a strict CSP.

Accepted slot values: `string | number` (text),
`false | true | null | undefined` (nothing), `` html`...` `` view, DOM
`Node`, or arrays of these.

## In practice

The Tabs block header, all four rules in twelve lines of shipped source
(`components/tabs`): a list as `.map()`, a live per-item class, a
snapshot per item, and a conditional "add" button as `&&`:

```javascript
<ul onclick="${onOpenEvent}" onkeydown="${onKeydown}">${() =>
    items.value.map(
        (item, i) =>
            html`<li class="lm-tabs-tab ${() => (selected.value === i ? 'lm-tabs-selected' : '')}"
                tabindex="0" role="tab" draggable="true"
                aria-selected="${() => (selected.value === i ? 'true' : 'false')}"
                data-icon="${item.icon || false}">${item.title || ''}</li>`
    )}</ul>
${() =>
    props.allowcreate!.value &&
    html`<div class="lm-tabs-insert-button"
        onclick="${() => create({ title: 'Untitled' }, null, true)}">add</div>`}
```

Inside the inner template `${item.title}` is a snapshot, correct, because
the outer arrow re-runs when `items` changes, while the class stays live
per item because selection changes without the list changing.

The same composability is why the datagrid can put **any block inside a
cell**: a column's `render` callback returns a string *or a view*, as in
`render: (value, row) => html`<${Rating} value="${value}" />``, and the
engine reconciles it like any other slot. No special cell-renderer API:
rule 4 is the API.

## Reference

```javascript
html`...`            // parse once per call site, package values; renders nothing
${state}             // live value
${() => expr}        // live expression (auto-tracked per run)
${plain}             // snapshot; strings are TEXT, never HTML
${() => x && html``} // branch        ${() => a.map(v => html``)}  // list
unsafe(htmlString)   // trusted markup opt-in → Node[]
```

- Slot positions: text content, full or partial quoted attribute value,
  component tag (`<${Card}>`). Anything else: `LJS-105`.
- Parse errors: `LJS-101` (mismatched close), `LJS-102` (unclosed).
  Dev tripwire: `LJS-202` (slot looks like a frozen snapshot).
- The values feeding templates are states; see [State](/docs/state/) and
  [computed()](/docs/computed/). For `on*` and `bind` attributes, see
  [Events](/docs/events/) and [Two-way binding](/docs/two-way-binding/).