Any \${...} in a Lit template can hold a JavaScript expression. This lesson covers exactly what kinds of values are valid there and how Lit renders each of them.
What Can Go Inside ${...}
A template expression can be any JavaScript expression: a variable, a function call, a ternary, or another html template result. Lit inspects the *type* of the resulting value at render time and decides how to render it — strings and numbers become text, TemplateResults render as nested DOM, arrays render each item in order, and null/undefined/nothing render as nothing.
Because expressions are just JavaScript, you can call helper functions, format dates, or compute derived values directly inline — there's no special template-only syntax to learn beyond the five binding types.
import { html } from 'lit';
const formatDate = (d) => new Intl.DateTimeFormat('en-US').format(d);
html`
<p>Posted on ${formatDate(new Date())}</p>
<p>Total: ${items.length} item${items.length === 1 ? '' : 's'}</p>
`;
Both expressions are ordinary JavaScript — a function call and a ternary — evaluated fresh on every render.
How Lit Renders Different Value Types
string / number -> rendered as text
TemplateResult -> rendered as nested DOM
array -> each item rendered in order
null / undefined -> renders nothing
nothing (from lit) -> explicitly renders nothing
DocumentFragment -> inserted directly
Numbers and strings are converted to text nodes, not raw HTML — no accidental HTML injection from plain text values.
Returning an array of TemplateResults renders each one in sequence, which is how .map() over data works.
null and undefined are treated the same as nothing — Lit removes any previously rendered content there.
Passing a raw HTML string does *not* get parsed as markup — use the unsafeHTML directive if you truly need that (with caution).
Expression Value Types Cheat Sheet
What happens for each kind of value placed in a text-position expression.
Value
Result
'hello'
Renders as the text "hello"
42
Renders as the text "42"
`html`<b>hi</b>`
Renders a real <b> element
[html`<li>A</li>`, html`<li>B</li>`]
Renders both list items in order
null / undefined
Renders nothing
nothing (from lit)
Explicitly renders nothing
A plain object {}
Renders its default toString(), usually not what you want
Text Expressions Are Escaped, Not Parsed as HTML
A common early mistake is expecting \${someHtmlString} to render markup. Lit treats any plain string in a text-position expression as literal text, escaping special characters, which is exactly what you want for untrusted or dynamic user content — it prevents injection by default.
const untrustedInput = '<script>alert(1)</script>';
html`<p>${untrustedInput}</p>`;
// Renders literally as text: <script>alert(1)</script>
// It does NOT execute as a script — this is a safety feature, not a bug.
Inline Computation vs. Extracted Helper Functions
Simple expressions are fine directly inside a template, but once logic grows past a one-liner, extract it into a private method or a plain function. This keeps render() readable and makes the logic independently testable.
#renderStatusLabel() {
if (this.status === 'loading') return 'Loading…';
if (this.status === 'error') return 'Something went wrong';
return 'Ready';
}
render() {
return html`<p>${this.#renderStatusLabel()}</p>`;
}
Common Mistakes
Expecting a plain string containing HTML tags to render as markup instead of escaped text.
Writing large, multi-branch conditional logic directly inline inside a template instead of extracting a helper method.
Interpolating an object directly and being surprised by [object Object] in the output.
Reaching for unsafeHTML out of habit instead of restructuring data into template results, which is almost always safer and just as easy.
Key Takeaways
Any JavaScript expression is valid inside ${...} — strings, numbers, template results, and arrays are all handled automatically.
Plain string/number values are rendered as escaped text, protecting against HTML injection by default.
Arrays of TemplateResults render as a sequence of DOM nodes, which is how list rendering works.
Extract non-trivial logic into helper methods to keep render() easy to read.
Pro Tip
If you ever feel tempted to reach for unsafeHTML, first ask whether the content could instead be expressed as data mapped through your own html templates — it's almost always possible, and avoids reintroducing the injection risks Lit protects you from by default.
You now understand how template expressions render different value types. Next, learn common patterns for conditional rendering.