| name | template-binding |
| description | The `template` option of `defineElement` with `{{name}}` / `{{@event}}` binding syntax. Load this when the task is about writing declarative HTML templates inside a custom element, using `{{...}}` for text/attributes, or `{{@event}}` to dispatch custom events. |
Template Binding
The template option of defineElement lets you write the element's markup as an HTML string with {{}} binding syntax. render() and template are mutually exclusive — pick one.
Imports
import { defineElement, StringAttr } from "@takanashi/rikka-elements";
import { h } from "@takanashi/rikka-dom";
Basic usage
import { defineElement, StringAttr } from "@takanashi/rikka-elements";
import { h } from "@takanashi/rikka-dom";
defineElement("my-card", {
attributes: { title: StringAttr, description: StringAttr },
events: { action: (e) => e.detail },
template: h`<template>
<div class="card">
<h2>{{title}}</h2>
<p>{{description}}</p>
<button onclick="{{@action}}">Action</button>
</div>
</template>`[0],
});
The template must be a real HTMLTemplateElement. Get it from h\`[0]`.
Binding syntax
| Syntax | Binding type | Behavior |
|---|
{{name}} | Text | textContent; checks $name signal first, falls back to raw name |
attr="{{name}}" | Attribute | setAttribute; reactive if value is a signal |
onclick="{{@action}}" | Event dispatch | DOM event → transform function → dispatchAction(detail) |
Text bindings — {{name}} auto-resolves
You only need to declare the attribute in attributes. The {{name}} placeholder auto-detects $name (signal) and uses it for fine-grained binding:
defineElement("my-counter", {
attributes: { clickCount: NumberAttr },
template: h`<template>
<p>Clicks: {{clickCount}}</p>
</template>`[0],
});
This is equivalent to writing render() { return p({}, this.$clickCount); } manually.
The lookup order:
this.$name (signal, fine-grained)
this.name (raw value, coarse-grained)
Attribute bindings — attr="{{name}}"
defineElement("my-input", {
attributes: { placeholder: StringAttr, value: StringAttr },
template: h`<template>
<input placeholder="{{placeholder}}" value="{{value}}">
</template>`[0],
});
If the bound expression is a signal, the attribute updates reactively.
Event dispatch — onclick="{{@action}}"
The {{@eventName}} syntax dispatches a custom event. The DOM event is passed to the events transform function, and the result becomes the detail:
defineElement("my-button", {
events: {
activate: (e: MouseEvent) => ({ x: e.clientX, y: e.clientY }),
},
template: h`<template>
<button onclick="{{@activate}}">Activate</button>
</template>`[0],
});
document.querySelector("my-button")!.onActivate = (ev) => {
console.log("Activated at", ev.detail.x, ev.detail.y);
};
The events key in the config is the source of truth. The template's {{@event}} references it by name.
Slots
<slot> elements in the template are projected from the host element's children:
defineElement("my-card", {
attributes: { title: StringAttr },
template: h`<template>
<div class="card">
<h2>{{title}}</h2>
<slot></slot>
</div>
</template>`[0],
});
const card = document.createElement("my-card");
card.setAttribute("title", "Hello");
card.innerHTML = "<p>Projected content</p>";
For named slots, use <slot name="header"> and slot="header" on the children.
Slots require Shadow DOM. With shadow: false, the <slot> element is left in the DOM but does not project children.
When to use template vs render
Use template when:
- The markup is mostly static, with just a few bindings
- You prefer writing HTML over composing
h() calls
- You want to declare a stable structure that survives data changes
Use render when:
- The structure itself depends on signals (e.g. different layouts for different modes)
- You need to compose with other
rikka-dom helpers (For, Show, function children)
- You want full programmatic control
You can mix both — but only one per element.
Pitfalls
1. Forgetting [0] on the template literal
template: h`<template><div>...</div></template>`,
template: h`<template><div>...</div></template>`[0],
2. {{name}} only works with declared attributes
The auto-detection works for declared attributes (this.$name from attributes). It does not resolve arbitrary nested paths:
template: h`<template><p>{{user.name}}</p></template>`[0];
3. Using onclick directly instead of {{@event}}
template: h`<template><button onclick="alert('x')">Go</button></template>`[0];
template: h`<template><button onclick="{{@go}}">Go</button></template>`[0],
events: { go: (e) => e.detail },
4. events key name vs {{@event}} name mismatch
The event name in the template must match the key in events (kebab-case preserved):
events: { "value-change": (e) => },
template: h`<template><button onclick="{{@value-change}}">X</button></template>`[0],
5. Mixing template and render
defineElement("my-el", {
template: h`<template>...</template>`[0],
render() { return div(); },
});
See also