In the previous lesson you interpolated your first value inside an html template, replacing fixed text with an instance field. But an interpolation inside ${} can do much more than insert plain text: it can fill in an HTML attribute, turn a boolean attribute on or off, directly assign a DOM element's property, or evaluate any arbitrary JavaScript expression. This lesson goes through, in detail, the different kinds of interpolation Lit supports, with concrete examples on <task-card>, and lays the syntactic groundwork you'll use throughout the rest of the course.
Contents
- The four destinations of an interpolation
- Text interpolation in a node's content
- Interpolation of regular HTML attributes
- Interpolation of DOM properties with the
.prefix - Boolean attributes with the
?prefix - Events with
@: a passing mention - Arbitrary JavaScript expressions inside
${} - Nesting
htmltemplates inside one another - Expanding
<task-card>with several fields
- The four destinations of an interpolation
When you write ${...} inside an html template, Lit needs to decide how to apply that value to the DOM, and the decision depends on where, syntactically, the interpolation has been placed. There are four main destinations, each with its own syntax:
| Destination | Syntax in the template | What it's for |
|---|---|---|
| Node content | <p>${valor}</p> |
Inserting text (or even another template) as an element's content |
| HTML attribute | <div id="${valor}"> |
Setting the value of an element's attribute, like any HTML attribute |
| DOM property | <input .value="${valor}"> |
Directly assigning a JavaScript property of the element, without going through an attribute |
| Boolean attribute | <button ?disabled="${valor}"> |
Adding or removing an attribute entirely depending on whether the value is "true" or "false" |
A fifth destination is added to these four: event handlers with the @ prefix (<button @click="${...}">), briefly presented in section 6 of this lesson, though its in-depth study belongs to module 5, "Events and Communication Between Components". The following sections develop each of the four main destinations.
- Text interpolation in a node's content
This is the kind of interpolation you already used in the previous lesson: placing ${...} directly between an element's opening and closing tags.
Lit inserts the value of this.titulo as the content of the <h3>, always treating it as plain text, never as HTML. This has an important consequence for security: if this.titulo contained, for example, the string '<script>alert(1)</script>', Lit would display it literally as visible text on screen (opening and closing tags included, as characters), instead of executing it as code. This is a deliberate difference from innerHTML, already discussed in the previous lesson, and it's the default and recommended behavior in the vast majority of cases.
This destination also accepts values that aren't text strings: numbers, true/false (shown literally as the text "true" or "false"), null and undefined (which show nothing), arrays of values (covered in the list-rendering lesson), and even the result of another nested html template, as explained in section 8.
- Interpolation of regular HTML attributes
When the interpolation appears inside the quotes of a standard HTML attribute's value, Lit treats it as an attribute assignment:
This destination is roughly equivalent to calling elemento.setAttribute('id', valor). It's the right destination for attributes that exist in HTML as text: id, class, title, href, src, custom data-* attributes, ARIA accessibility attributes, etc. One detail worth noting: if the interpolated value is undefined, Lit removes the attribute entirely instead of setting it with the text "undefined"; this behavior is useful for optional attributes, though module 7 presents a specific directive (ifDefined) designed precisely to express this intent more explicitly.
- Interpolation of DOM properties with the
. prefix
. prefixSometimes you're not interested in setting an HTML attribute (which is always text), but in directly assigning a JavaScript property of the DOM element, which can hold any type of value: an object, an array, a native boolean, a function. For this, Lit offers the syntax with a dot in front of the name:
Here, .value="${this.textoBusqueda}" doesn't create or modify any value attribute in the HTML; instead, it directly executes elemento.value = this.textoBusqueda, assigning the <input> element's JavaScript property. The difference between attribute and property can seem subtle, but it's important: many native browser elements (like <input>) keep their original HTML attribute (the one written in the markup) separate from their current JavaScript property (the value the user has typed into the field), and in many cases you're interested in working with the property, not the attribute.
As a quick reference for deciding which to use:
| Situation | Recommended destination |
|---|---|
| The value is always a simple text string and it makes sense for it to appear in the HTML | Attribute (attr="${valor}") |
| The value is an object, an array, or any non-text data type | Property (.prop="${valor}") |
You're working with a specific DOM property that has no exact HTML attribute equivalent (such as .value in advanced forms) |
Property (.prop="${valor}") |
This same mechanism, passing properties (not just attributes) to an element, is also the foundation of how a parent Lit component will pass complex data (objects, arrays) to a child Lit component; this will be revisited in detail in module 5 when discussing communication between components.
- Boolean attributes with the
? prefix
? prefixSome HTML attributes are "boolean" in a special sense: their mere presence on the tag turns the behavior on, regardless of the text assigned to them. disabled, checked, hidden or required are common examples. Writing disabled="false" in plain HTML, in fact, does not disable the button: the attribute is still present, so the browser interprets it as disabled anyway.
To handle this kind of attribute correctly, Lit offers the syntax with the ? prefix:
With ?disabled="${this.estaBloqueado}", Lit adds the disabled attribute to the element only if this.estaBloqueado is a "truthy" value (true, or any value JavaScript treats as truthy), and removes it entirely from the element if it's "falsy" (false, null, undefined, 0, empty string...). This reproduces exactly the real semantics of HTML boolean attributes: what matters isn't the attribute's text, but whether it's present or not.
- Events with
@: a passing mention
@: a passing mentionThere's a fifth kind of interpolation, with the @ prefix, meant for attaching DOM event handlers to an element:
This syntax is roughly equivalent to elemento.addEventListener('click', this.manejarClic). It's mentioned here only so you recognize the syntax if you see it in examples or in Lit's official documentation during this module; its full study —including how to correctly define the handler method, the problem of the value of this inside it, and how to dispatch custom events between components— is the entire content of module 5, "Events and Communication Between Components". For now, none of this module's templates will use event handlers yet.
- Arbitrary JavaScript expressions inside
${}
${}Something worth internalizing as soon as possible: inside ${} you can place not only bare variable or property names (${this.titulo}); you can place any valid JavaScript expression that produces a value. This includes arithmetic operations, text concatenation, method calls, ternary operators, and any combination of them:
render() {
return html`
<article>
<h3>${this.titulo.toUpperCase()}</h3>
<p>Prioridad: ${this.prioridad + 1} de 5</p>
<p>${this.completada ? 'Tarea finalizada' : 'Tarea pendiente'}</p>
</article>
`;
}In this example, ${this.titulo.toUpperCase()} calls the string's toUpperCase() method before interpolating the result; ${this.prioridad + 1} performs an arithmetic operation; and ${this.completada ? 'Tarea finalizada' : 'Tarea pendiente'} uses a ternary operator to choose between two texts based on a condition. This last pattern, the ternary operator inside an interpolation, is so common in Lit that the next lesson, "Conditional Rendering", develops it in depth as the main technique for showing or hiding content.
The only thing you cannot do inside ${} is write full JavaScript statements (an if with braces, a for loop, a const declaration): only expressions are allowed, that is, code that reduces to a single value. When more elaborate logic than a simple expression is needed, the usual technique is to extract it into a method or helper function of the class, and call that method from within ${}, as is done with ${this.titulo.toUpperCase()} in the example above.
- Nesting
html templates inside one another
html templates inside one anotherAs noted in section 2, the result of a call to html (a TemplateResult object) is a perfectly valid value to interpolate inside another html template. This lets you split a large template into smaller fragments and combine them:
render() {
const cabecera = html`<h3>${this.titulo}</h3>`;
return html`
<article>
${cabecera}
<p>Estado: ${this.estado}</p>
</article>
`;
}Here, cabecera is itself the result of a call to html, and it's interpolated inside the main template exactly as any other value would be. Lit recognizes that it's another template (not plain text) and inserts it as real HTML, not as escaped text. This ability to nest templates is the foundation of two techniques studied in the upcoming lessons: extracting conditional fragments into helper functions (conditional rendering lesson) and generating one template per element of an array (list rendering lesson).
- Expanding
<task-card> with several fields
<task-card> with several fieldsWith all the interpolation types now covered, it's time to expand <task-card> to show several task fields at once: title, status, and priority. Remember, as explained in the previous lesson, that these are still plain instance fields, not reactive properties yet.
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
constructor() {
super();
this.titulo = 'Preparar la demo del sprint';
this.estado = 'En curso';
this.prioridad = 2;
this.urgente = false;
}
render() {
return html`
<article ?data-urgente="${this.urgente}">
<h3>${this.titulo}</h3>
<p>Estado: ${this.estado}</p>
<p>Prioridad: ${this.prioridad} de 5</p>
<p>${this.urgente ? '⚠ Requiere atención inmediata' : 'Sin urgencia especial'}</p>
</article>
`;
}
}
customElements.define('task-card', TaskCard);Let's go over what's new:
${this.titulo},${this.estado}and${this.prioridad} de 5are simple text interpolations, as already seen in the previous lesson; the priority one combines the interpolated value with additional literal text inside the same node.?data-urgente="${this.urgente}"uses the boolean attribute syntax seen in section 5: thedata-urgenteattribute will only appear on the<article>whenthis.urgenteistrue. This is useful, for example, so CSS styles (covered in module 4) can apply a different look to urgent cards based on the presence of that attribute.- The last line uses a ternary operator, as explained in section 7, to show different text depending on the value of
this.urgente.
When you reload the page, the card will now show several combined pieces of data. Just like in the previous lesson, if you modify these fields from the browser console, the screen won't update automatically: that piece still belongs to module 3.
Common Mistakes and Tips
- Using
.propwhen a regular attribute would be enough: if the value is always simple text, there's no need to use the dot syntax; the regular attribute (attr="${valor}") is more readable and sufficient in most cases. - Using a regular attribute for a boolean: writing
disabled="${this.estaBloqueado}"(without the?) doesn't produce the expected result, because Lit would convert the value to text and assign it as an attribute with that text, instead of adding or removing the attribute based on its condition. For real boolean attributes, always use?attr. - Trying to fit a full statement inside
${}: code like${if (this.urgente) { return 'Sí'; }}is not valid, because${}only accepts expressions, not statements. The correct alternative is a ternary operator or extracting the logic into a separate method. - Forgetting that text content is automatically escaped: if you need to show real, dynamically generated HTML content (not simple text), interpolating directly isn't enough; there's a specific mechanism for that exact case (the
unsafeHTMLdirective) that will be studied, along with its security risks, in later modules. By default, and in 99% of cases, text interpolation is the correct and safest choice.
Exercises
- Add to
<task-card>a new instance fieldthis.etiqueta(for example,'Backend') and interpolate it as the value of the<article>'sdata-etiquetaattribute. - Add a field
this.progreso(a number from 0 to 100) and show it interpolated inside a paragraph along with the percent symbol, for example: "Progreso: 40%". - Modify the boolean interpolation of
this.urgentefrom section 9 so that, instead of adata-urgenteattribute, it controls the real booleanhiddenattribute of a small<span>with the text "URGENTE" (that is, so the<span>only appears when the task is urgent).
Solutions
render() {
return html`
<article>
...
<span ?hidden="${!this.urgente}">URGENTE</span>
</article>
`;
}Note the use of negation (!this.urgente): since hidden hides the element when present, the attribute must be added precisely when the task is not urgent, hence the negation of the original value.
Conclusion
In this lesson you went through the different destinations an interpolation can have inside an html template: text content, regular HTML attributes, DOM properties with the . prefix, and boolean attributes with the ? prefix, plus a passing mention of event handlers with @, which will be detailed in module 5. You also saw that inside ${} you can place any JavaScript expression, not just bare variable names, and that html templates can be nested inside one another. With all this, <task-card> now combines several interpolated fields: title, status, priority, and an urgency indicator.
In the next lesson, "Conditional Rendering", you'll focus on one of the expressions you've already started using in passing —the ternary operator inside ${}— and develop it in depth as the main technique for showing different content based on conditions, applying it to a status badge in <task-card> that will change depending on whether the task is pending, in progress, or done.
Lit Course
Module 1: Introduction to Lit and Web Components
- What are Web Components and why Lit?
- Setting Up the Development Environment
- Your First Lit Component
- Anatomy of a Lit Component
Module 2: Reactive Templates and Rendering
- Lit's Template Engine
- Expressions and Interpolation in Templates
- Conditional Rendering
- List Rendering
- The Rendering Cycle
Module 3: Reactive Properties and State
- Reactive Properties
- Internal State with @state
- Types of Properties and Custom Converters
- Attributes vs Properties and Reflection
Module 4: Styling Lit Components
- Encapsulated CSS with Shadow DOM
- Shared Styles Between Components
- Custom CSS Properties and Theming
- Slots and Styling Distributed Content
Module 5: Events and Component Communication
- Handling DOM Events in Templates
- Custom Events: Communication from Child to Parent
- Communication from Parent to Child with Properties
- Communication Patterns Between Sibling Components
Module 6: Lifecycle and Advanced Behavior
- Lifecycle Callbacks
- Reactive Hooks: willUpdate, updated, and firstUpdated
- Reactive Controllers
- Mixins and Composing Behavior
Module 7: Directives and Advanced Template Features
- Built-in Directives: classMap, styleMap and ifDefined
- Custom Directives
- Asynchronous Rendering with until
- Shared Context with @lit/context
Module 8: Integration, Interoperability and Deployment
- Using Lit Components in Plain HTML
- Integrating Lit with React, Vue, and Angular
- Server-Side Rendering with @lit-labs/ssr
- Bundling, Publishing, and TypeScript
Module 9: Testing and Best Practices
- Unit Tests with Web Test Runner
- Accessibility in Web Components
- Performance and Optimization
- Common Patterns and Anti-patterns
