Template syntax
The rule
Section titled “The rule”A function in a ${} is a live binding. A value is written once.
html`<p>${count}</p>` // livehtml`<p>${count()}</p>` // a snapshothtml`<p>${() => a() + b()}</p>` // live, derivedThe exceptions are @event and ref, where a function is always used
literally — a handler is a handler.
Every form
Section titled “Every form”| Syntax | Binds |
|---|---|
<p>${value}</p> |
child: text, number, node, template, array, or each |
title=${value} |
attribute on a native element (removed when null/undefined/false); property on a custom element |
class="btn ${kind}" |
attribute built from static text plus values |
.value=${value} |
DOM property, casing preserved |
?disabled=${value} |
attribute present only while truthy |
@click=${fn} |
event listener |
ref=${fn} |
called with the element |
On a custom element, name=${name} and .name=${name} do the same thing:
they set the property, so objects, arrays and camelCase names round-trip when a
parent passes props down. Use the dot when you want a native property
(.value on an input, .hidden) or when the name would otherwise stay an
attribute (data-*, aria-*). Pass the signal, not name() — calling is a
snapshot and the child will not see later writes.
Child values
Section titled “Child values”| Value | Renders |
|---|---|
'text', 42 |
a text node |
null, undefined, true, false, '' |
nothing |
a Node |
the node itself |
html`…` |
a nested template |
| an array | each item in order |
each(...) |
a list with per-row scopes |
Event modifiers
Section titled “Event modifiers”| Modifier | Effect |
|---|---|
.stop |
stopPropagation() |
.prevent |
preventDefault() |
.once |
fires at most once |
.capture |
capture phase |
.passive |
passive listener |
They compose: @click.stop.prevent=${fn}.
Bindings without modifiers are delegated — one listener per render root rather than one per element. A modifier opts out and attaches a real listener.
Delegated handlers only see events that bubble. Real user events always do;
new Event('click') does not unless you pass { bubbles: true }.
class=${'a b'} // stringclass=${['a', cond && 'b']} // array; falsy entries droppedclass=${{ a: true, b: cond() }} // object; truthy keys includedclass=${() => ({ on: active() })} // liveNested arrays and objects are flattened. An empty result removes the attribute.
style=${'color: red'} // stringstyle=${{ color: 'red', width: '10px' }} // objectstyle=${{ '--tone': color() }} // custom propertiesstyle=${() => ({ opacity: fade() })} // liveA key you stop passing is cleared. Custom properties are set with
setProperty, so they cascade into children and across shadow boundaries.
Not supported
Section titled “Not supported”html`<${tag}>` // dynamic tag nameshtml`<div ${attrName}="x">` // dynamic attribute nameshtml`<div ${spreadProps}>` // spreadinghtml`<textarea>${value}</textarea>` // use .valuehtml`<title>${value}</title>` // use .textContentAn attribute containing a binding must be quoted or whole:
html`<div class="a ${b}">` // finehtml`<div class=${b}>` // finehtml`<div class=a${b}>` // not supportedValues interpolated inside an HTML comment are ignored.
The compiler throws a SyntaxError for unsupported bindings rather than
guessing, so these fail loudly at first render.