Bindings — text & attributes
A binding is a spot in your template that comes from JavaScript. Remember the one rule: wrap it in () => to make it live.
Text bindings#
const name = createState("world");
html`<p>Hello ${() => name.get()}</p>`; // live
html`<p>Hello ${"static text"}</p>`; // static, set onceA live text binding updates the same text node in place — no element is recreated.
Text is always safe#
Interpolated text is rendered as inert text, never as HTML:
html`<p>${() => userInput.get()}</p>`;
// if userInput is "<img src=x onerror=alert(1)>", it shows as literal text.
// Zoijs does NOT execute it. XSS-safe by default.Attribute bindings#
const cls = createState("box");
const busy = createState(false);
html`<div class=${() => cls.get()}>...</div>`; // string attribute
html`<button disabled=${() => busy.get()}>Save</button>`; // boolean attribute- Strings set the attribute.
truesets a present-but-empty boolean attribute (disabled).false/null/undefinedremove the attribute entirely.
Partial and multiple holes work#
html`<div class="card ${() => theme.get()} ${() => size.get()}">...</div>`;
html`<a href=${() => base.get()} title="Go to ${() => page.get()}">link</a>`;Styling with an object#
Bind style to a plain object and each property is applied through the CSSOM — safe from injection (a value can't break out of the attribute or add extra declarations). camelCase keys are hyphenated, custom properties pass through:
html`<div style=${() => ({ width: pct.get() + "%", backgroundColor: color.get() })}>...</div>`;A dynamic style string still works, but prefer the object form for anything derived from data — see Security.
Form values use the property#
value and checked are bound to the element property, so they reflect correctly even after the user types or clicks:
html`<input value=${() => draft.get()} />`;
html`<input type="checkbox" checked=${() => done.get()} />`;URLs are checked#
URL attributes (href, src, …) reject dangerous schemes like javascript: automatically.
Static vs live — a quick reference#
| You write | Behavior |
|---|---|
${() => state.get()} | Live — updates on change |
${someValue} | Static — set once |
attr=${() => state.get()} | Live attribute |
attr="constant" | Static attribute |
Element refs#
Sometimes you need the actual DOM element — to focus it, measure it, draw into a <canvas>, or hand it to a third-party library. The ref binding gives it to you:
html`<input ref=${(el) => el.focus()} />`;The callback runs once, just after the element is inserted (so it's connected — focus, scroll, measure, and canvas contexts all work). It is not reactive: the function is read once, never re-run. ref is a binding semantic, not an export — there's nothing new to import.
If the callback returns a function, that's a cleanup — Zoijs runs it when the element is unmounted or removed from a list. This is how you tie an observer, a timer, or a library instance to the element's lifetime:
const box = (el) => {
const ro = new ResizeObserver(() => console.log(el.clientWidth));
ro.observe(el);
return () => ro.disconnect(); // runs on unmount — no leak
};
html`<div ref=${box}>…</div>`;A non-function value is ignored (with a dev-mode warning) and never becomes a ref attribute — so an inert string can't be wired up. Refs work inside keyed each lists too. See the Charts recipe for the library-bridge pattern and Infinite scroll for an IntersectionObserver owned by a ref.
SVG#
SVG works inside html as long as it's wrapped in <svg>…</svg>. Dynamic attributes (including xlink:href) are handled.
html`<svg viewBox="0 0 20 20"><circle cx="10" cy="10" r=${() => radius.get()} /></svg>`;Next: Events.