- How
html`...`works and why it's called a "tagged template" - Automatic HTML escaping for interpolated values
- Boolean attribute shorthand:
disabled="${value}" - Conditional rendering with ternaries and
&& - List rendering with
.map() - When to use
unsafe()(and when not to)
Basic interpolation
Use ${expression} inside a template to embed reactive state into HTML:
template() {
return html`
<h1>Welcome, ${this.state.username}</h1>
<p>You have ${this.state.unread} unread messages.</p>
`;
}
Numbers, strings, booleans — all primitive values are converted to strings automatically.
Automatic escaping (XSS-safe by default)
Every interpolated value is HTML-escaped. User input cannot inject HTML:
// If this.state.comment is '<script>alert("xss")</script>'
template() {
return html`<div>${this.state.comment}</div>`;
}
// Renders as: <div><script>alert("xss")</script></div>
Security baseline
You don't need to remember to escape. The template does it automatically. XSS via interpolation is not possible.
Nested templates
html tagged templates can nest. Each nested template is a valid HtmlString:
template() {
const badge = (count) => html`<span class="ck-badge">${count}</span>`;
return html`<div>Inbox ${badge(this.state.unread)}</div>`;
}
Boolean attribute shorthand
HTML boolean attributes like disabled, checked, selected, and hidden get special handling:
template() {
return html`
<input disabled="${this.state.isLocked}">
<div hidden="${this.state.isEmpty}">No results</div>
`;
}
When the value is true, the attribute is present (bare). When false, the attribute is removed entirely:
this.state.isLocked === true → <input disabled>
this.state.isLocked === false → <input>
Conditional rendering
Use JavaScript ternary or && for conditional branches:
template() {
return html`
${this.state.loggedIn
? html`<a href="/profile">Profile</a>`
: html`<a href="/login">Log in</a>`}
${this.state.error && html`<div class="ck-alert ck-alert--error">${this.state.error}</div>`}
`;
}
List rendering with .map()
Map over arrays. Each item can return a nested template:
template() {
return html`
<ul>
${this.state.items.map((item) => html`
<li data-key="${item.id}">${item.name}</li>
`)}
</ul>
`;
}
Tip: Use data-key for stable identity
Add data-key to list items when they can be reordered, inserted, or deleted. The morph engine uses it to preserve DOM nodes instead of recreating them.
Escaping helpers: escapeHtml() and unsafe()
escapeHtml(str) escapes & < > " '. Normally you don't need it because templates escape automatically — but it's useful when building strings outside a template.
unsafe(rawHtml) bypasses escaping. Use only with independently sanitized HTML:
import { html, unsafe, escapeHtml } from '.../core.min.js';
// Only use unsafe() with vetted HTML
const sanitized = DOMPurify.sanitize(userProvidedHtml);
template() {
return html`<div>${unsafe(sanitized)}</div>`;
}
unsafe() requires security review
Per ADR-0003, every call to unsafe() must be independently reviewed. Prefer escaping by default.
Recap
Always return html`...`
template() must return an HtmlString. The html tag creates one.
Auto-escaped
All interpolated values are escaped. XSS via ${} is not possible.
Boolean shorthand
disabled="${val}" emits the bare attribute when true, removes it when false.
Nest, map, branch
Templates compose. Use ternaries, &&, and .map() freely.