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

  1. The four destinations of an interpolation
  2. Text interpolation in a node's content
  3. Interpolation of regular HTML attributes
  4. Interpolation of DOM properties with the . prefix
  5. Boolean attributes with the ? prefix
  6. Events with @: a passing mention
  7. Arbitrary JavaScript expressions inside ${}
  8. Nesting html templates inside one another
  9. Expanding <task-card> with several fields

  1. 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.

  1. 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.

render() {
  return html`<h3>${this.titulo}</h3>`;
}

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.

  1. 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:

render() {
  return html`<article id="${this.identificador}" class="${this.claseCss}"></article>`;
}

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.

  1. Interpolation of DOM properties with the . prefix

Sometimes 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:

render() {
  return html`<input .value="${this.textoBusqueda}">`;
}

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.

  1. Boolean attributes with the ? prefix

Some 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:

render() {
  return html`<button ?disabled="${this.estaBloqueado}">Guardar</button>`;
}

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.

  1. Events with @: a passing mention

There's a fifth kind of interpolation, with the @ prefix, meant for attaching DOM event handlers to an element:

render() {
  return html`<button @click="${this.manejarClic}">Completar tarea</button>`;
}

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.

  1. 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.

  1. Nesting html templates inside one another

As 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).

  1. Expanding <task-card> with several fields

With 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 5 are 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: the data-urgente attribute will only appear on the <article> when this.urgente is true. 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 .prop when 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 unsafeHTML directive) 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

  1. Add to <task-card> a new instance field this.etiqueta (for example, 'Backend') and interpolate it as the value of the <article>'s data-etiqueta attribute.
  2. 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%".
  3. Modify the boolean interpolation of this.urgente from section 9 so that, instead of a data-urgente attribute, it controls the real boolean hidden attribute 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 data-etiqueta="${this.etiqueta}">
      ...
    </article>
  `;
}
render() {
  return html`
    <article>
      ...
      <p>Progreso: ${this.progreso}%</p>
    </article>
  `;
}
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

Module 2: Reactive Templates and Rendering

Module 3: Reactive Properties and State

Module 4: Styling Lit Components

Module 5: Events and Component Communication

Module 6: Lifecycle and Advanced Behavior

Module 7: Directives and Advanced Template Features

Module 8: Integration, Interoperability and Deployment

Module 9: Testing and Best Practices

Module 10: Project: Building TaskFlow

© Copyright 2026. All rights reserved