Skip to content

Lit Templates

Lit renders UI using the html tagged template function, powered by the lit-html library. This lesson explains how templates are written, parsed, and efficiently updated.

What Is a Lit Template?

A Lit template is the result of calling the html tagged template function with a template literal containing your markup and any dynamic \${...} expressions. The html function doesn't return a string — it returns a lightweight TemplateResult object describing the static markup and the dynamic values separately.

The first time a template renders, lit-html parses the static HTML into a real <template> element and clones it. On every subsequent render, it skips re-parsing entirely and instead diffs only the dynamic expression values against their previous values, updating just the DOM nodes that changed.

import { html } from 'lit';

function greet(name) {
  return html`<p>Hello, ${name}!</p>`;
}

greet('Ada') returns a TemplateResult, not a string — Lit knows exactly where name sits in the DOM tree, so updates never involve re-parsing HTML.

Where Expressions Can Appear

html`
  <p>${textContent}</p>
  <div class=${classAttr} ?hidden=${isHidden}></div>
  <input .value=${propValue} @input=${onInput} />
  ${condition ? html`<span>Yes</span>` : html`<span>No</span>`}
`;
  • Text position: ${value} inside element content renders text or another template result.
  • Attribute position: attr=${value} sets a plain HTML attribute (always stringified).
  • Boolean attribute: ?attr=${value} adds/removes the attribute based on truthiness.
  • Property binding: .prop=${value} sets a JS property directly on the element (no stringification).
  • Event binding: @event=${handler} attaches an event listener.

Template Binding Cheat Sheet

The five binding syntaxes lit-html supports, at a glance.

Syntax Binds To Example
${value} Text content or child template <p>${msg}</p>
attr=${value} HTML attribute <img src=${url}>
?attr=${value} Boolean attribute <button ?disabled=${busy}>
.prop=${value} JavaScript property <input .value=${text}>
@event=${fn} Event listener <button @click=${onClick}>
html`...` Nested template ${cond ? html`<a>` : ''}

Templates Must Be Static Template Literals

lit-html relies on being able to identify the static parts of your template at parse time, so the string parts of an html`...` call must be a literal written directly in your source code — never built by concatenating strings or interpolating markup fragments dynamically as text.

// Wrong — builds markup as a string, defeats lit-html's diffing
const tag = isPrimary ? 'button' : 'a';
html`<${tag}>Click</${tag}>`; // invalid: tag name can't be dynamic

// Right — branch between two full, static templates
html`${isPrimary ? html`<button>Click</button>` : html`<a>Click</a>`}`;

Element and attribute *names* must be static; only attribute/property *values* and text content can be dynamic.

Nesting Templates and Rendering Lists

Any ${...} expression can itself return another TemplateResult, which is how you compose smaller templates into larger ones. Arrays of template results are also supported directly — Lit renders each item and keeps the array structure in the DOM.

const items = ['Tea', 'Coffee', 'Water'];

html`
  <ul>
    ${items.map((item) => html`<li>${item}</li>`)}
  </ul>
`;

For lists that reorder or change, prefer the repeat() directive (covered later) so Lit can track items by a stable key instead of by index.

Common Mistakes

  • Building markup with string concatenation and passing the result into a template, bypassing lit-html's diffing entirely.
  • Trying to make a tag name or attribute name dynamic — only values inside a fixed structure can change.
  • Forgetting the difference between attr=${value} (string attribute) and .prop=${value} (JS property), leading to values silently being stringified.
  • Mapping over an array without a stable key for reorderable lists, causing unnecessary DOM churn — use repeat() instead.

Key Takeaways

  • The html tagged template function returns a TemplateResult, not a string.
  • lit-html parses static markup once and only diffs dynamic expression values on subsequent renders.
  • Five binding types cover almost every use case: text, attribute, boolean attribute, property, and event.
  • Element/attribute names must stay static; only values and text content can change dynamically.

Pro Tip

When unsure which binding to use, default to .prop=${value} for anything that isn't a plain string attribute (booleans, objects, arrays) — property bindings avoid HTML's string-only attribute limitation entirely.