Lit templates support four distinct binding syntaxes beyond plain text content. This lesson goes deep on each one: attribute, boolean attribute, property, and event bindings.
Four Binding Prefixes, Four Behaviors
Beyond binding text content with ${value}, Lit templates support three special prefix characters on the left-hand side of an attribute-like binding — ? for booleans, . for JS properties, and @ for events — plus plain unprefixed attribute bindings for ordinary string attributes.
Choosing the right prefix matters: using a plain attribute binding for a boolean, or a property binding when a plain string would do, are both common sources of confusing bugs for developers new to Lit.
Plain attribute bindings always stringify their value, exactly like setAttribute().
Boolean bindings add the attribute when the value is truthy and remove it entirely when falsy, matching native HTML boolean attribute semantics.
Property bindings set the actual JavaScript property on the element instance, bypassing string conversion entirely — required for values like arrays, objects, or Date instances.
Event bindings call addEventListener once and reuse it across renders unless the handler function reference itself changes.
Binding Syntax Cheat Sheet
Which prefix to reach for, with concrete examples.
Prefix
Example
Use When
(none)
title=${tooltip}
The value is naturally a string
?
?disabled=${isBusy}
The value is a boolean controlling presence
.
.value=${text}
Setting form control values, objects, or arrays
.
.items=${list}
Passing complex data into a child Lit component
@
@click=${onClick}
Attaching any DOM event listener
@
@my-custom-event=${onCustom}
Listening for a custom event dispatched by a child
Property Bindings Between Custom Elements
Property bindings (.prop=${value}) are especially important when passing data between Lit components, because attributes can only carry strings. Passing an array or object as a plain attribute would require JSON-stringifying and parsing it, which is slower and error-prone; a property binding passes the reference directly.
attribute: false combined with a property binding is the standard pattern for passing rich data down to child components.
Event Handler Identity and Re-Renders
Lit compares the event handler function reference between renders. If you pass a brand-new arrow function on every render, Lit still only attaches one listener at a time (it removes the old one and adds the new one), but for performance-sensitive lists, defining the handler once (e.g. as a bound class method) avoids that extra add/remove work entirely.
// Re-created every render — Lit swaps the listener each time
@click=${() => this.#handleClick(item.id)}
// Defined once as a stable class method reference
@click=${this.#handleClick}
For most components this micro-optimization doesn't matter; consider it only in large, frequently re-rendered lists.
Common Mistakes
Using a plain attribute binding for a boolean value, resulting in disabled="false" (still present, so still disabled) instead of the attribute being removed.
Passing an array or object through a plain string attribute instead of a property binding, requiring manual JSON encoding.
Forgetting the . prefix when trying to set a JS-only property (like .value on an <input>), and wondering why the plain attribute binding didn't work as expected for pre-filled form values.
Attaching event listeners manually with addEventListener inside render() instead of using the declarative @event=${handler} binding.
Key Takeaways
Plain attribute bindings stringify values; boolean (?), property (.), and event (@) bindings each have distinct, specific behavior.
Use property bindings (.) to pass non-string data like arrays and objects, especially into child Lit components.
Use boolean bindings (?) for anything that should follow HTML's presence-based boolean attribute convention.
Event bindings (@) are the idiomatic way to attach listeners — avoid manual addEventListener calls in render().
Pro Tip
When passing data to a custom child element, default to a property binding (.) unless you have a specific reason (like needing the value visible in the DOM for CSS or debugging) to use a plain attribute instead.
You now understand all four Lit binding syntaxes in depth. Next, learn about the special nothing and noChange sentinel values.