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.
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.
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.
You now understand how Lit templates work under the hood. Next, look closely at the render() method itself.