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
- General structure of a LitElement class
- Declaring properties: decorators versus
static properties - LitElement is HTMLElement: the relationship with the standard
- Shadow DOM by default: what it means and what it implies
- Overview of a Lit component's lifecycle
- Towards module 2: reactive templates
- 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.
- Declaring properties: decorators versus
static properties
static propertiesIn 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.
- 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:
HTMLElementis the base class that all DOM elements inherit from, both the browser's native ones (HTMLButtonElement,HTMLDivElement...) and any Custom Element.ReactiveElementis an intermediate class that Lit adds to bring in the reactive property system and the update cycle, without yet imposing a specific templating engine.LitElementinherits fromReactiveElementand adds therender()method together with the use of thehtmlfunction as its templating engine.- Your class (
TaskCard, in the example) inherits fromLitElementand 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.
- 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 viadocument.querySelectorfrom outside the component; you have to accesselemento.shadowRootfirst 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.
- 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
connectedCallbackanddisconnectedCallbackcallbacks 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.
- 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.querySelectorfrom 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 itsshadowRoot(elemento.shadowRoot.querySelector('h3')), or better yet, avoid manipulating the internal DOM by hand and letrender()decide what gets shown. - Forgetting to call
super()in the constructor: if you define a customconstructorwithout callingsuper()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
ReactiveElementwithLitElement: in day-to-day work you almost never interact directly withReactiveElement; it is mentioned here only to understand the inheritance chain. In practice, every component in this course will extendLitElementdirectly.
Exercises
- Draw (on paper or in a text file) the full inheritance chain of a
UserAvatarcomponent that you define yourself, fromHTMLElementdown to your class, indicating what each link in the chain contributes. - Take the
task-card.jscomponent from the previous lesson (the static version) and add aconnectedCallback()method that, usingconsole.log, writes the message "task-card conectado al DOM" to the browser console. Remember to callsuper.connectedCallback(). - Explain in your own words, in two or three sentences, why the fact that
LitElementuses Shadow DOM by default is an advantage for a project like TaskFlow, which will have several different components coexisting on the same page.
Solutions
- The chain, just like that of
TaskCardseen 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.
- 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
- 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
