Skip to content
DevinimJS
Phase 1 · Lesson 3

HTML Templates — The html Tag

The html tagged template is the only template mechanism in DevinimJS. Learn its rules once, build everything with it.

What you'll learn
  • 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>&lt;script&gt;alert("xss")&lt;/script&gt;</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>`}
  `;
}
Live demo — toggle visibility

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.