In the previous lesson you created your first Lit component without dwelling too much on why each piece was there. Before moving on to reactive templates, it is worth pausing to understand in detail the full anatomy of a class that extends LitElement: what parts make it up, how it relates to the Custom Elements standard you already know, what it implies that it uses Shadow DOM by default, and what moments of life a component goes through from the time it is born until it disappears. This lesson is the last theoretical one in the module and closes the circle opened in the first lesson, leaving the ground ready for module 2, where you will start working with truly dynamic templates.

Contents

  1. General structure of a LitElement class
  2. Declaring properties: decorators versus static properties
  3. LitElement is HTMLElement: the relationship with the standard
  4. Shadow DOM by default: what it means and what it implies
  5. Overview of a Lit component's lifecycle
  6. Towards module 2: reactive templates

  1. General structure of a LitElement class

Every class that extends LitElement can generally contain the following blocks (none are mandatory except render(), which is the only one required for the component to show anything):

import { LitElement, html, css } from 'lit';

class TaskCard extends LitElement {

  // 1. Declaring reactive properties (detailed in module 3)
  static properties = {
    titulo: { type: String },
  };

  // 2. Encapsulated styles (detailed in module 4)
  static styles = css`
    :host {
      display: block;
      border: 1px solid #ccc;
    }
  `;

  // 3. Constructor: initializing internal state
  constructor() {
    super();
    this.titulo = 'Tarea sin título';
  }

  // 4. Lifecycle callbacks (detailed in module 6)
  connectedCallback() {
    super.connectedCallback();
  }

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

customElements.define('task-card', TaskCard);

Don't worry if some of these pieces (static properties, static styles, connectedCallback) don't make much sense yet: they will be explained in depth in modules 3, 4 and 6 respectively. The goal of this overview is that, by the time you reach those modules, you will already have a clear picture of the class's general "shape" and know where to place each new piece within a familiar scheme. In fact, notice that the task-card.js component from the previous lesson was simply a version of this very same structure in which only block 5 (render()) was filled in; all the other blocks are optional and get added according to the component's needs.

One detail about the constructor: if you define one, you must call super() first, as with any JavaScript class that inherits from another. It is the usual place to initialize default values for the component's properties, something that will be revisited in more detail in module 3.

  1. Declaring properties: decorators versus static properties

In the previous lesson you already saw that there are two styles for registering a component: customElements.define and the @customElement decorator. That same duality of styles repeats itself when declaring a component's reactive properties (their full study belongs to module 3, but it is worth recognizing the syntax already, since it will appear in examples throughout this course and in Lit's official documentation).

With decorators (requires build support, as explained in the environment setup lesson):

import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('task-card')
class TaskCard extends LitElement {
  @property({ type: String })
  titulo = 'Tarea sin título';

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

With static properties, in plain JavaScript with no additional build step:

import { LitElement, html } from 'lit';

class TaskCard extends LitElement {
  static properties = {
    titulo: { type: String },
  };

  constructor() {
    super();
    this.titulo = 'Tarea sin título';
  }

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

customElements.define('task-card', TaskCard);
Aspect With decorators (@property) With static properties
Requires additional build step Yes (TypeScript or Babel with decorator plugin) No, standard JavaScript
Where the default value is declared Right next to the property declaration itself In the constructor, after super()
Verbosity More compact Slightly more verbose
Runtime result Identical Identical

Both styles produce exactly the same behavior; the choice is a matter of preference and of the tools available in the project. This course, as already explained in the environment setup lesson, will mainly use static properties, showing the decorator variant as a reference whenever it is useful for those working with TypeScript in their own projects.

  1. LitElement is HTMLElement: the relationship with the standard

It is essential to internalize an idea that was already hinted at in the module's first lesson: LitElement does not replace the Custom Elements standard, it extends it. In JavaScript terms, the inheritance chain is:

HTMLElement  →  ReactiveElement  →  LitElement  →  TaskCard (your component)
  • HTMLElement is the base class that all DOM elements inherit from, both the browser's native ones (HTMLButtonElement, HTMLDivElement...) and any Custom Element.
  • ReactiveElement is an intermediate class that Lit adds to bring in the reactive property system and the update cycle, without yet imposing a specific templating engine.
  • LitElement inherits from ReactiveElement and adds the render() method together with the use of the html function as its templating engine.
  • Your class (TaskCard, in the example) inherits from LitElement and therefore inherits all the capabilities of the three preceding classes.

The practical consequence of this inheritance chain is that a Lit component is, literally, an HTMLElement. This means it inherits the entire standard API of a DOM element: you can add and remove attributes (setAttribute, getAttribute), you can listen for and dispatch events (addEventListener, dispatchEvent, something that is put to full use in the events module), it has access to classList, and it participates in the DOM tree exactly like any other element. None of this is a simulation or a separate layer: it is the web platform itself.

  1. Shadow DOM by default: what it means and what it implies

In the previous lesson, when inspecting <task-card> in the developer tools, a #shadow-root node appeared that may have been surprising. This happens because LitElement, by default, internally calls attachShadow({ mode: 'open' }) on every instance of the component and renders the result of render() inside that Shadow DOM, not directly as children of the element in the regular DOM.

// This is, in simplified form, what LitElement does for you
// the first time the component needs to render:
const raizSombra = this.attachShadow({ mode: 'open' });
raizSombra.innerHTML = /* result of render() */;

What does this imply in practice, without yet going into the detail of styles (which belongs to module 4)?

  • Encapsulation of the internal HTML: the content returned by render() is not visible via document.querySelector from outside the component; you have to access elemento.shadowRoot first and search inside it.
  • Encapsulation of styles: any CSS declared inside the component (with static styles, as seen in section 1) does not leak out to the rest of the page, and the page's global styles do not accidentally leak into the component either. This avoids one of the most common problems when building large interfaces: CSS class name collisions between different components.
  • Distributed content with <slot>: Shadow DOM also defines a mechanism, <slot>, that lets a component receive HTML content "from the outside" and position it within its own internal structure. This mechanism will be explored in detail in the styles module.

For now, the key point to remember is this: every Lit component lives, by default, in its own bubble isolated from the rest of the page, both in structure and in styles. This is one of the reasons why Lit is especially well suited for building libraries of reusable components like those in TaskFlow: you can combine <task-card>, <task-list> and <user-avatar> without fear that the styles of one will interfere with those of another.

  1. Overview of a Lit component's lifecycle

A Lit component, just like any Custom Element, does not exist statically: it is born, it possibly gets updated many times, and at some point it may disappear from the DOM. Lit provides a series of methods ("lifecycle callbacks") that run automatically at different moments of that life, and that a component can override to run its own logic at the right moment.

This lesson only presents a general map, with the name of each callback and roughly what it is for; the detailed workings, with real usage examples, are studied in depth in module 6 ("Lifecycle and Advanced Behavior").

Moment in the lifecycle Callback What it is roughly for
The element is inserted into the DOM connectedCallback() Inherited directly from the Custom Elements standard; a good place to subscribe to external events or start timers
The element is removed from the DOM disconnectedCallback() Also from the standard; the usual place to "clean up" whatever was started in connectedCallback (cancel subscriptions, timers...)
Before applying an update willUpdate(changedProperties) Specific to Lit; lets you compute derived values right before rendering, based on which properties have changed
The new template is generated render() The only mandatory one; returns the component's current HTML
After applying an update to the DOM updated(changedProperties) Specific to Lit; useful for reacting to changes already reflected in the real DOM
Right after the first update firstUpdated(changedProperties) Specific to Lit; runs only once, ideal for initializations that need the internal DOM to already exist

Two important ideas to keep in mind, even though the detail is still to come:

  • The connectedCallback and disconnectedCallback callbacks are not a Lit invention: they belong to the Custom Elements standard seen in the module's first lesson. Lit makes use of them and adds its own layer of additional callbacks (willUpdate, updated, firstUpdated) specific to its reactive rendering system.
  • No component needs to implement all of these callbacks. Most simple components, like the static <task-card> from the previous lesson, need none of them; they get added as the component needs to react to specific moments of its life.

  1. Towards module 2: reactive templates

This lesson closes the introductory module. You now know the foundation on which everything else rests: what Web Components are as a standard, what Lit brings on top of them, how to set up a work environment, what a LitElement class looks like on the inside, and what moments of life a component goes through.

However, all the examples seen so far share a deliberate limitation: their templates are completely static. The <task-card> you have built always shows the same sample task, no matter what happens in the application. That limitation is exactly the starting point of module 2, "Reactive Templates and Rendering," where you will learn to insert dynamic expressions inside the html function, to show or hide content based on conditions, to render complete lists of tasks, and to understand how and when Lit decides to run render() again.

Common Mistakes and Tips

  • Trying to access the component's internal content with document.querySelector from the outside: since the content lives inside a Shadow DOM, document.querySelector('task-card h3') will not find anything. You have to access the element itself first and then its shadowRoot (elemento.shadowRoot.querySelector('h3')), or better yet, avoid manipulating the internal DOM by hand and let render() decide what gets shown.
  • Forgetting to call super() in the constructor: if you define a custom constructor without calling super() first, the browser will throw a runtime error. This rule is not specific to Lit, but to any JavaScript class that inherits from another.
  • Thinking you need to implement all the lifecycle callbacks: as shown in the table in section 5, most components only need render(). Adding callbacks "just in case" complicates the component unnecessarily.
  • Confusing ReactiveElement with LitElement: in day-to-day work you almost never interact directly with ReactiveElement; it is mentioned here only to understand the inheritance chain. In practice, every component in this course will extend LitElement directly.

Exercises

  1. Draw (on paper or in a text file) the full inheritance chain of a UserAvatar component that you define yourself, from HTMLElement down to your class, indicating what each link in the chain contributes.
  2. Take the task-card.js component from the previous lesson (the static version) and add a connectedCallback() method that, using console.log, writes the message "task-card conectado al DOM" to the browser console. Remember to call super.connectedCallback().
  3. Explain in your own words, in two or three sentences, why the fact that LitElement uses Shadow DOM by default is an advantage for a project like TaskFlow, which will have several different components coexisting on the same page.

Solutions

  1. The chain, just like that of TaskCard seen in section 3, is:
HTMLElement (browser standard: base API of any DOM element)
  → ReactiveElement (Lit: reactive property system and update cycle)
    → LitElement (Lit: templating engine via render() and html)
      → UserAvatar (your class: the specific logic and template for the avatar)
import { LitElement, html } from 'lit';

class TaskCard extends LitElement {
  connectedCallback() {
    super.connectedCallback();
    console.log('task-card conectado al DOM');
  }

  render() {
    return html`
      <article>
        <h3>Preparar la demo del sprint</h3>
        <p>Estado: En curso</p>
        <p>Asignada a: Ana</p>
      </article>
    `;
  }
}

customElements.define('task-card', TaskCard);

When you reload the page with the developer tools open, the message should appear in the console for every <task-card> present in the HTML.

  1. One possible answer: since each component is encapsulated in its own Shadow DOM, the CSS styles of <task-card>, <task-list> or <user-avatar> cannot interfere with each other or with the page's global styles, even if they use the same class or tag names. This allows each component to be developed and maintained independently, with no need to coordinate CSS naming conventions across the whole team, something especially valuable as TaskFlow keeps adding more components throughout the course.

Conclusion

In this lesson you finished taking apart the anatomy of a Lit component: its general structure with optional blocks and a single mandatory method (render()), the two ways of declaring properties (decorators and static properties), its direct relationship with HTMLElement through the inheritance chain HTMLElement → ReactiveElement → LitElement, the use of Shadow DOM by default to encapsulate structure and styles, and a first overview — just the names and their general purpose — of the lifecycle that will be detailed in module 6.

This closes the introductory module. In module 2, "Reactive Templates and Rendering," you will leave static components behind: you will learn to insert dynamic expressions in the html function, to conditionally render content, to display lists of items, and to understand in depth Lit's rendering cycle, giving <task-card>, <task-list> and the rest of TaskFlow's pieces their first real layer of dynamic life.

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