Throughout this module you have been using, without dwelling on it too much, a distinction that is worth pinning down precisely before closing the topic: the difference between an HTML attribute and a JavaScript property. This lesson explains that difference in detail, clarifies which direction Lit synchronizes by default and when it is worth turning on synchronization in the opposite direction with reflect: true, and closes the module by applying everything learned to turn tareas, on <task-list>, into a reactive property of type Array. With that final step, every <task-card> on the board will finally show the data for its own task, solving the limitation that has followed TaskFlow since module 2.

Contents

  1. Attribute and property: two different things that are often confused
  2. The default synchronization: from attribute to property
  3. reflect: true: synchronizing in the opposite direction too
  4. When reflect: true is worth it (and its connection to module 4)
  5. Turning tareas into a reactive property of <task-list>
  6. Passing each task's own data to every <task-card>: TaskFlow, finally, dynamic
  7. Closing the module: on to styles

  1. Attribute and property: two different things that are often confused

It is common, especially coming from static HTML, to assume that "attribute" and "property" are the same thing. On the web platform, however, they are two related but distinct concepts, and the distinction is key to precisely understanding Lit's reactivity system.

  • An attribute is a piece of text that appears in the HTML markup, between an element's opening tags: <task-card titulo="Revisar el PR">. As explained in the previous lesson, an attribute is always a text string, without exception, because that is how the HTML standard defines it.
  • A property is a field of the JavaScript object that represents that element in the DOM: elemento.titulo. A property can hold any JavaScript data type: text, number, boolean, array, object, even a function.

This distinction is not exclusive to Lit or to Custom Elements: it exists on any native browser element. An <input> has a value attribute (text) and a value property (also text, in this particular case, but by the element's own design decision); an <input type="checkbox"> has a checked attribute (whose mere presence indicates "checked", as seen with Boolean in the previous lesson) and a checked property that is a real JavaScript boolean. Lit does not invent this mechanism: it adopts it and generalizes it for the reactive properties declared in static properties.

What static properties does is, precisely, tell Lit: "I want these two things — the titulo attribute and the titulo property — to be connected automatically, and this is how you should convert between one and the other."

  1. The default synchronization: from attribute to property

By default, when you declare a reactive property with type (or with state: true, although in that case, as seen in the previous lesson, there is no attribute at all to synchronize with), synchronization happens in a single direction: from attribute to property.

static properties = {
  titulo: { type: String },
};

With this declaration, if the titulo attribute changes in the HTML — either because the element is created with that attribute, or because elemento.setAttribute('titulo', 'Nuevo valor') is called afterward —, Lit detects that change and automatically updates this.titulo with the converted value. This direction is, in fact, the one used in every HTML example from the previous lessons of this module (<task-card titulo="...">).

But the reverse path does not happen by default: if elemento.titulo = 'Otro valor' is assigned from JavaScript, the property changes (and triggers the render() update, as seen throughout the module), but the titulo attribute in the HTML is not updated to reflect that new value. If you inspected the element with the browser's development tools at that moment, the attribute would still show the original value, even though the internal property and what is shown on screen had already changed.

const tarjeta = document.querySelector('task-card');
// The original HTML was: <task-card titulo="Preparar la demo"></task-card>

tarjeta.titulo = 'Revisar el PR';
// this.titulo is now 'Revisar el PR', and render() updates.
// But the HTML attribute is still titulo="Preparar la demo".

  1. reflect: true: synchronizing in the opposite direction too

When you want that reverse path to also work — for a change in the property, made from JavaScript, to be reflected back into the HTML attribute —, you turn it on with the reflect: true option, already mentioned in passing in the module's first lesson:

static properties = {
  estado: { type: String, reflect: true },
};

With reflect: true, every time this.estado changes (through any path: direct assignment, dot interpolation in a parent template, etc.), Lit automatically updates the element's estado attribute in the real DOM to match the property's new value, internally using a conversion equivalent to toAttribute (the same function seen in the custom converter from the previous lesson, although for the types supported out of the box Lit already carries its own conversion logic with no need to write it by hand).

const tarjeta = document.querySelector('task-card');
tarjeta.estado = 'hecha';
// With reflect: true, the DOM attribute becomes:
// <task-card estado="hecha">

It is important to point out that reflect: true adds extra work on every update (Lit has to call setAttribute on the real DOM element, in addition to updating the property's internal field), so it is not worth turning it on "just in case" on every property; it is reserved for the specific cases where it truly adds value, covered in the next section.

  1. When reflect: true is worth it (and its connection to module 4)

In practice, there are two main reasons why it is worth turning on reflect: true on a property:

  • Being able to select the element by its state from outside, for example with document.querySelector('task-card[estado="hecha"]'), occasionally useful for automated tests or for external tools that inspect the DOM without direct access to the JavaScript properties.
  • Being able to apply different CSS styles depending on the property's value, using an attribute selector, something much more common in practice: if the estado attribute is always reflected in the DOM, you can write a rule in the component's own stylesheet like :host([estado="hecha"]) { opacity: 0.6; }, which applies a different style based on the card's current state, without needing to compute CSS classes dynamically in the template.

This second reason is, by far, the most relevant one for TaskFlow, and it is exactly the bridge to the next module in the course: in module 4, "Styling Lit Components", you will see in detail how to write selectors like :host([atributo]) and why reflecting a property like estado (or urgente) turns out to be very useful for giving a different visual style to an urgent card or to one that is already completed, without needing to manage CSS classes by hand from render(). For now, in this module, it is enough to know that reflect: true exists, exactly what it does, and that its main usefulness will show up in the next module.

  1. Turning tareas into a reactive property of <task-list>

With all the theory of reactive properties already covered in this module, it is time to close the limitation that has followed TaskFlow since the "Rendering Lists" lesson in module 2: <task-list> loops over an array this.tareas, but that array is a plain, non-reactive instance field, and every <task-card> it generates looks identical because it receives no data of its own.

Pick up src/components/task-list.js and declare tareas as a reactive property of type Array, following exactly the same pattern used with <task-card> throughout the module:

import { LitElement, html } from 'lit';
import './task-card.js';

class TaskList extends LitElement {
  static properties = {
    tareas: { type: Array },
  };

  constructor() {
    super();
    this.tareas = [
      { id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso', prioridad: 4, urgente: true },
      { id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente', prioridad: 2, urgente: false },
      { id: 3, titulo: 'Desplegar a producción', estado: 'hecha', prioridad: 5, urgente: false },
    ];
  }

  render() {
    return html`
      <section>
        <h2>Mis tareas</h2>
        <div class="lista">
          ${this.tareas.map(
            (tarea) => html`<task-card></task-card>`
          )}
        </div>
      </section>
    `;
  }
}

customElements.define('task-list', TaskList);

On its own, this change already adds something: since tareas is now a genuinely reactive property, you could assign it a completely new array from outside (listaElemento.tareas = [...]) and <task-list> would update itself, regenerating the whole list of cards. But, as explicitly pointed out when <task-list> was built in module 2, one final piece is still missing: passing each task's data to its corresponding <task-card>.

  1. Passing each task's own data to every <task-card>: TaskFlow, finally, dynamic

With <task-card> already equipped with real reactive properties (titulo, estado, prioridad, urgente) since the first lesson of this module, it is now possible to complete render() in <task-list> using the dot-property syntax introduced in module 2's lesson on expressions and interpolation:

render() {
  return html`
    <section>
      <h2>Mis tareas</h2>
      <div class="lista">
        ${this.tareas.map(
          (tarea) => html`
            <task-card
              .titulo="${tarea.titulo}"
              .estado="${tarea.estado}"
              .prioridad="${tarea.prioridad}"
              .urgente="${tarea.urgente}"
            ></task-card>
          `
        )}
      </div>
    </section>
  `;
}

Let's analyze, step by step, what happens now every time <task-list> renders. this.tareas.map(...) loops over the array of tasks, exactly as explained in the "Rendering Lists" lesson; for each task, it generates a <task-card> template in which four properties are set using dot syntax (.titulo, .estado, .prioridad, .urgente), each interpolating the matching field from the current tarea object in the loop. Since those four properties are declared as reactive on <task-card> (since this module's first lesson), setting them through dot syntax establishes them as real JavaScript properties on each instance, not as text attributes, allowing the number (prioridad) and the boolean (urgente) to be passed directly with no manual conversion at all.

The result, on reloading the page, is what has been expected since module 2: <task-list> shows three cards, each with its own title, its own status badge (computed by renderInsigniaEstado() from the received estado property), its own priority and, where relevant, its own urgency warning. Every <task-card> is now a genuinely reusable component: the same TaskCard class, parameterized differently on each instance according to the data it receives from its parent.

This pattern — a parent component (<task-list>) that loops over a collection and passes a slice of data to each child component (<task-card>) through properties — is the standard way components communicate from parent to child in Lit, and in practically every modern UI library. Communication in the opposite direction — for <task-card> to notify <task-list> that the user has marked a task as completed, for example — requires a different mechanism, based on custom events, which is exactly the content of module 5, "Events and Communication Between Components". For now, with parent-to-child communication already solved, TaskFlow already has a fully dynamic task board as far as showing data goes, although still with no way for the user to interact with it beyond the expand click seen in the previous lesson.

  1. Closing the module: on to styles

This lesson completes module 3, "Properties and Reactive State". The journey has gone from simpler to more advanced: first, declaring real reactive properties with static properties and understanding what makes them trigger automatic updates; then, distinguishing between public properties and private internal state with state: true; next, the full catalog of supported types and how to write a custom converter for types that do not fit that catalog; and, in this final lesson, the exact relationship between attributes and properties, along with the reflect option to synchronize in both directions.

TaskFlow, as a result of the whole module, has taken a qualitative leap: <task-card> now has complete reactive properties (titulo, estado, prioridad, urgente, fechaLimite), its own internal state (expandida), and <task-list> receives its collection of tasks as a reactive property of type Array and passes each child card the data that belongs to it. The board, for the first time in the course, shows genuinely different data on each card and updates itself whenever that data changes.

However, if you have been following the course by running the examples in the browser, you will have noticed that, despite all this data reactivity, the visual appearance of <task-card> and <task-list> is still that of unstyled HTML: no colors, no careful spacing, no clear visual distinction between an urgent card and one that is not. The data is already dynamic; what is missing is for it to look good. That is precisely the job of module 4, "Styling Lit Components", where you will learn to use static styles and the css function to give TaskFlow's components an encapsulated, coherent appearance, and where you will revisit the attribute reflection seen in this lesson in order to apply different styles depending on each card's state or urgency through selectors like :host([estado="hecha"]).

Common Mistakes and Tips

  • Expecting a property change from JavaScript to update the attribute without reflect: true: as explained in section 2, the default synchronization only goes from attribute to property; if the reverse path is needed, it has to be turned on explicitly with reflect: true, and only on properties where it truly adds value.
  • Turning on reflect: true on every property without needing to: as noted in section 3, reflecting adds a setAttribute call on every update; on properties that will never be used as a CSS selector or inspected from outside, that extra synchronization is wasted work.
  • Confusing dot syntax (.propiedad) with a regular attribute: as noted in section 6, .titulo="${tarea.titulo}" assigns the JavaScript property directly, without going through text or through any attribute converter; it is the recommended way to pass non-text data types (numbers, booleans, arrays, objects) from a parent component to a child component.
  • Passing a full array of tasks by mutating the existing array instead of reassigning it: if a task is later added to <task-list> with this.tareas.push(nuevaTarea) instead of this.tareas = [...this.tareas, nuevaTarea], the reactive tareas property will not detect the change, for exactly the reason explained in this module's first lesson about mutation versus reassignment.

Exercises

  1. Add a fourth task to <task-list>'s initial array, with its own titulo, estado, prioridad and urgente, and check that the fourth <task-card> generated correctly shows its own data, different from the other three.
  2. Declare the estado property on <task-card> with reflect: true (in addition to type: String, which it already had) and check, by inspecting the element with the browser's development tools after assigning elemento.estado = 'hecha' from the console, that the DOM's estado attribute also changes to "hecha".
  3. Explain in your own words, drawing on section 4, why reflecting the estado property (with reflect: true) makes more practical sense in TaskFlow than reflecting, for example, the date-typed fechaLimite property.

Solutions

this.tareas = [
  { id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso', prioridad: 4, urgente: true },
  { id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente', prioridad: 2, urgente: false },
  { id: 3, titulo: 'Desplegar a producción', estado: 'hecha', prioridad: 5, urgente: false },
  { id: 4, titulo: 'Actualizar la documentación', estado: 'pendiente', prioridad: 1, urgente: false },
];

On reloading the page, Array.map generates a fourth <task-card> template with its own .titulo, .estado, .prioridad and .urgente properties interpolated from this new object, showing data different from the previous three cards, without touching the rest of render().

static properties = {
  titulo: { type: String },
  estado: { type: String, reflect: true },
  prioridad: { type: Number },
  urgente: { type: Boolean },
  expandida: { state: true },
  fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
};

After running elemento.estado = 'hecha' in the console, inspecting the element in the development tools shows that the tag now displays <task-card estado="hecha" ...>, because reflect: true makes Lit automatically call setAttribute('estado', 'hecha') on the element every time the property changes.

  1. Reflecting estado makes practical sense because, as explained in section 4, it allows writing attribute-based CSS selectors (:host([estado="hecha"])) to give a different visual style to cards depending on their state, something that will be put to use in module 4, and it also allows locating cards by their state from outside with selectors like task-card[estado="hecha"]. fechaLimite, by contrast, is an almost continuous value (a specific date, potentially different for every task) that rarely makes sense to use as a CSS selector or as an exact-match attribute filter; moreover, its custom converter already turns the date into text in a way designed for Lit to read, not necessarily for comfortable use as a selector, so reflecting it would add little practical value against the extra cost of synchronizing it on every update.

Conclusion

In this final lesson of module 3 you have finished pinning down the distinction between attribute (text, in the HTML) and property (any type, in JavaScript), understanding that Lit synchronizes by default only from attribute to property, and that reflect: true also turns on the reverse path for the cases where it is worth it — typically, to be able to apply CSS styles based on a property's value, as will be put to use in module 4. On that foundation, you have turned tareas, on <task-list>, into a reactive property of type Array, and you have completed <task-list>'s template to pass each task's data to its corresponding <task-card> through dot-property syntax, getting every card on the board to finally show the information for its own task.

This closes the module "Properties and Reactive State": <task-card> and <task-list> are now genuinely reactive components, with their own data, internal state, and a well-solved parent-to-child communication. Now that the data is reactive and every card shows the right information, it is time to deal with how they look: in module 4, "Styling Lit Components", you will learn to give visual style to TaskFlow's components.

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