Templates
Elur uses JavaScript tagged template literals as its templating primitive.
The html tag function parses the template string at runtime, creates the DOM
nodes, and wires up reactivity. No compiler, no JSX, no special file format.
# Basic syntax
import { html } from "@elurjs/core";
html`<h1>Hello, world!</h1>`.mount("#app");The html tag returns an ElurTemplate object. Call .mount(target) to
insert it into the DOM.
# Text interpolation
Use ${...} to insert dynamic values. For reactive values, pass a
function:
const name = signal("Alice");
html`<p>Hello, ${() => name.value}!</p>`.mount("#app");When name changes, only the text node updates. Static values can be
interpolated directly:
html`<p>2 + 2 = ${2 + 2}</p>`;# Attribute interpolation
For reactive attributes, the interpolation must cover the entire attribute value at runtime:
// ✅ Correct — entire value is a single interpolation
html`<div class=${() => `card ${active.value ? "active" : ""}`}>Content</div>`;
// ❌ Not supported at runtime — partial interpolation
html`<div class="card ${() => active.value}">Content</div>`;Partial attribute interpolation (class="btn ${size}") is handled at compile
time by @elurjs/vite-plugin-elur. Without the plugin, only full bindings
(class=${value}) are supported. This keeps the runtime parser simple and fast.
# Boolean attributes
For boolean attributes like disabled, checked, hidden:
html`<button disabled=${() => loading.value}>Submit</button>`;When the value is null, undefined, or false, the attribute is removed
entirely. Any other value (including 0 and "") is set as a string.
# DOM properties
For value, checked, and selected on form elements, Elur sets the DOM
property directly (not the HTML attribute). This ensures form state stays
in sync with the signal:
html`<input value=${() => text.value} @input=${(e) => text.value = e.target.value} />`;
html`<input type="checkbox" checked=${() => done.value} />`;# URL sanitization
URL attributes (href, src, action, formaction, etc.) are sanitized to
prevent javascript: and other dangerous protocols. This runs automatically —
no configuration needed.
# Event handling
Use the @ prefix for event listeners:
html`<button @click=${() => count.value++}>Increment</button>`;You can use any DOM event name: @input, @change, @submit, @keydown,
@mouseenter, etc.
html`<input
@input=${(e: Event) => text.value = (e.target as HTMLInputElement).value}
/>`;# Event modifiers
Append . modifiers to an event name to add behavior without wrapper
functions:
// Prevent default and stop propagation
html`<form @submit.prevent=${() => save()}></form>`;
// Only fire on Enter
html`<input @keydown.enter=${() => submit()} />`;Supported modifiers:
| Modifier | Effect |
|---|---|
.prevent |
Calls e.preventDefault() before the handler |
.stop |
Calls e.stopPropagation() before the handler |
.self |
Only fires if e.target is the element itself |
.enter, .escape, .space, .tab, .delete, .backspace, .up, .down, .left, .right |
Keyboard key filters (only fires if the matching key was pressed) |
Single character (.a, .b, ...) |
Fires only if e.key matches that character |
Modifiers can be combined: @keydown.prevent.enter=${handler}.
# Conditional rendering
Use a ternary inside a reactive interpolation. Returning null or false
removes the content; returning a template inserts it:
html`
${() =>
loading.value
? html`<p>Loading…</p>`
: html`<p>Data: ${() => data.value}</p>`}
`;# show / hide attributes
For showing/hiding an element without unmounting it, use the show or
hide attribute. These toggle display: none and preserve the element's
original display value:
// Element stays in the DOM; only display is toggled
html`<div show=${() => visible.value}>Content</div>`;
html`<div hide=${() => loading.value}>Content</div>`;show displays the element when the value is truthy; hide displays it when
the value is falsy. Both accept a static value or a reactive function.
# List rendering
Use repeat() for keyed list rendering:
import { html, signal, repeat } from "@elurjs/core";
const items = signal([
{ id: 1, text: "Learn Elur" },
{ id: 2, text: "Build an app" },
]);
html`
<ul>
${() =>
repeat(
items.value,
(item) => item.id, // key function
(item) => html`<li>${item.text}</li>` // render function
)}
</ul>
`;repeat() performs key-based reconciliation: items that are added, removed,
or reordered are handled efficiently without re-rendering the entire list.
# Nested templates
Templates compose naturally:
function Button(label: string, onClick: () => void) {
return html`<button @click=${onClick}>${label}</button>`;
}
html`
<div>
${Button("Save", () => save())}
${Button("Cancel", () => cancel())}
</div>
`;# Refs
Use ref() to get a direct reference to a DOM element:
import { html, ref } from "@elurjs/core";
const inputEl = ref<HTMLInputElement>();
html`<input ref=${inputEl} placeholder="Type here" />`.mount("#app");
// Access the element after mount:
inputEl.el?.focus();Always use a function for reactive interpolations: ${() => value.value},
not ${value.value}. The latter reads the signal once and never updates.