Ha llegado el momento de escribir código de verdad. En esta lección crearás tu primer componente con Lit, empezando por el clásico "Hola Mundo" y avanzando enseguida hacia algo con más sentido dentro de este curso: la primera versión de <task-card>, el componente que representará una tarea individual en TaskFlow. Esta primera versión será deliberadamente simple —sin propiedades ni datos dinámicos— para centrarse en entender la mecánica básica de un componente Lit: cómo se define la clase, cómo se declara su plantilla y cómo se registra como una etiqueta HTML utilizable.

Contenido

  1. Los tres pasos de todo componente Lit
  2. Hola Mundo con LitElement
  3. Registrar el componente: customElements.define y @customElement
  4. Usar el componente como una etiqueta HTML normal
  5. Primera versión de <task-card>
  6. Verlo funcionar en la página

  1. Los tres pasos de todo componente Lit

Todo componente hecho con Lit, por complejo que llegue a ser más adelante en el curso, se construye siempre siguiendo los mismos tres pasos:

  1. Extender LitElement: se crea una clase de JavaScript que hereda de LitElement, la clase base que proporciona Lit.
  2. Implementar render(): dentro de esa clase se define un método render() que devuelve la plantilla HTML del componente, usando la función html.
  3. Registrar el elemento: se le da un nombre de etiqueta al componente mediante customElements.define (o el decorador equivalente @customElement), para que el navegador sepa qué clase debe usar cuando encuentre esa etiqueta en el HTML.

El resto de esta lección desarrolla cada uno de estos tres pasos con ejemplos concretos.

  1. Hola Mundo con LitElement

Empecemos por el ejemplo más simple posible. Dentro del proyecto taskflow creado en la lección anterior, crea un fichero src/components/hola-mundo.js con el siguiente contenido:

import { LitElement, html } from 'lit';

class HolaMundo extends LitElement {
  render() {
    return html`<p>¡Hola Mundo desde Lit!</p>`;
  }
}

customElements.define('hola-mundo', HolaMundo);

Analicemos cada línea:

  • import { LitElement, html } from 'lit';: se importan las dos piezas que se van a usar. LitElement es la clase base de la que hereda cualquier componente Lit; html es una función especial (una "función de plantilla etiquetada", una característica estándar de JavaScript) que permite escribir HTML de forma legible dentro del código JavaScript.
  • class HolaMundo extends LitElement { ... }: se declara una clase normal de JavaScript que hereda de LitElement. A partir de aquí, HolaMundo tiene ya todas las capacidades que ofrece Lit (renderizado reactivo, ciclo de vida, etc.), aunque en este primer ejemplo no se use ninguna de las más avanzadas.
  • render() { return html\

    ¡Hola Mundo desde Lit!

    `; }: el método render()es el corazón de cualquier componente Lit. Lit lo invoca automáticamente cuando el componente necesita mostrarse (o actualizarse) y espera que devuelva el resultado de la funciónhtml`. Aquí, simplemente se devuelve un párrafo con un texto fijo.
  • customElements.define('hola-mundo', HolaMundo);: esta línea registra la clase HolaMundo como la implementación de la etiqueta <hola-mundo>. A partir de este momento, cualquier <hola-mundo> que aparezca en el HTML de la página será "activado" por el navegador usando esta clase.

Un detalle importante sobre la función html: aunque a simple vista parezca una simple cadena de texto, no lo es. html es una plantilla etiquetada que Lit analiza de forma especial, lo que le permite, entre otras cosas, saber exactamente qué partes de la plantilla son fijas y cuáles son dinámicas (esto se explota a fondo en el módulo de plantillas reactivas). Por ahora, con una plantilla completamente estática como esta, basta con saber que el contenido entre los acentos graves (`) es el HTML que se va a mostrar.

  1. Registrar el componente: customElements.define y @customElement

La llamada a customElements.define('hola-mundo', HolaMundo) no es una particularidad de Lit: es la forma estándar en la que la especificación de Custom Elements (vista en la lección anterior) registra cualquier elemento personalizado, con o sin Lit de por medio. Lit no sustituye este mecanismo, simplemente lo usa.

Existe una forma alternativa de escribir este registro, mediante un decorador, si el proyecto tiene soporte para decoradores (como es el caso de la plantilla de Lit de Vite vista en la lección anterior):

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

@customElement('hola-mundo')
class HolaMundo extends LitElement {
  render() {
    return html`<p>¡Hola Mundo desde Lit!</p>`;
  }
}

Ambas formas son equivalentes: @customElement('hola-mundo') es simplemente una manera más concisa de escribir, justo encima de la clase, lo mismo que customElements.define('hola-mundo', HolaMundo) hace después de ella. La tabla siguiente resume cuándo conviene cada estilo:

Estilo Ventaja Cuándo usarlo
customElements.define(...) No requiere ninguna configuración especial de compilación; funciona en JavaScript puro sin pasos adicionales Proyectos en JavaScript plano, sin Babel/TypeScript configurado para decoradores
@customElement(...) Más conciso y legible; mantiene el nombre de la etiqueta pegado a la definición de la clase Proyectos en TypeScript, o en JavaScript con Babel configurado para soportar decoradores (como la plantilla de Vite)

Un requisito importante, común a ambos estilos y heredado directamente del estándar de Custom Elements: el nombre de la etiqueta debe contener siempre al menos un guion (hola-mundo, task-card, user-avatar...). Esta regla existe para garantizar que las etiquetas personalizadas nunca choquen con las etiquetas nativas del HTML actuales o futuras, que nunca llevan guion.

Este curso usará mayoritariamente customElements.define, por ser la forma que funciona sin ninguna configuración adicional, mostrando @customElement como alternativa cuando aporte claridad.

  1. Usar el componente como una etiqueta HTML normal

Una vez registrado, <hola-mundo> se usa exactamente igual que cualquier etiqueta nativa de HTML. No hace falta "montarlo" mediante una función especial ni pasarle nada a través de JavaScript para que aparezca: basta con escribirlo en el HTML.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>TaskFlow</title>
  <script type="module" src="/src/components/hola-mundo.js"></script>
</head>
<body>
  <h1>Mi primera prueba con Lit</h1>
  <hola-mundo></hola-mundo>
</body>
</html>

Dos detalles a tener en cuenta:

  • El <script> que carga el componente debe tener type="module", porque el fichero usa import, una característica de los módulos ES estándar de JavaScript.
  • El elemento <hola-mundo></hola-mundo> se puede colocar en cualquier parte del <body>, tantas veces como se quiera, igual que se haría con un <div> o un <button>. Cada aparición de la etiqueta crea una instancia independiente del componente.

  1. Primera versión de <task-card>

Con la mecánica básica ya clara, es el momento de crear el primer componente real de TaskFlow: <task-card>. En esta lección será una versión deliberadamente simple y totalmente estática: mostrará siempre el mismo contenido de ejemplo, sin ninguna propiedad configurable ni ningún dato dinámico. La capacidad de personalizar cada tarjeta con datos reales (título, estado, persona asignada...) llegará con las propiedades reactivas, en el módulo 3.

Crea el fichero src/components/task-card.js:

import { LitElement, html } from 'lit';

class TaskCard extends LitElement {
  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);

Este componente sigue exactamente los mismos tres pasos vistos en el apartado 1: hereda de LitElement, implementa render() devolviendo una plantilla html (en este caso con varias etiquetas anidadas: un <article> que envuelve un título y dos párrafos) y se registra con customElements.define bajo el nombre task-card.

Para usarlo, se puede crear una pequeña página de prueba, index.html, en la raíz del proyecto (si usas la estructura de Vite, sustituye el contenido de ejemplo que trae por defecto):

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>TaskFlow</title>
  <script type="module" src="/src/components/task-card.js"></script>
</head>
<body>
  <h1>TaskFlow</h1>
  <task-card></task-card>
</body>
</html>

  1. Verlo funcionar en la página

Con el servidor de desarrollo arrancado (npm run dev, como se explicó en la lección anterior) y el navegador abierto en la URL local, deberías ver la tarjeta renderizada en la página. Un ejercicio útil en este punto es abrir las herramientas de desarrollo del navegador (F12) e inspeccionar el elemento <task-card>: si Shadow DOM estuviera activado (todavía no lo hemos usado explícitamente, ya que no se ha llamado a attachShadow ni se ha usado la plantilla dentro de un render con Shadow DOM propio de Lit), verías el contenido del <article> como hijos directos del elemento en el DOM normal ("light DOM").

Es importante aclarar un matiz aquí: aunque en este ejemplo no se ha mencionado explícitamente, LitElement usa Shadow DOM por defecto para renderizar el contenido devuelto por render(). Es decir, al inspeccionar <task-card> en las herramientas de desarrollo, en realidad verás una etiqueta especial #shadow-root dentro del elemento, y el <article> colgando de ella, no directamente del <task-card>. Esto es exactamente lo esperado: es el comportamiento por defecto de cualquier componente Lit. El siguiente tema de este mismo módulo explica con más detalle qué implica esto y por qué es así; de momento, si ves ese #shadow-root en el inspector, es la confirmación de que todo funciona correctamente.

Errores Comunes y Consejos

  • Olvidar type="module" en el <script>: si el componente no aparece y la consola del navegador muestra un error relacionado con import, lo primero que hay que comprobar es que el <script> que carga el fichero tiene el atributo type="module".
  • Nombre de etiqueta sin guion: customElements.define('taskcard', TaskCard) lanzará un error en tiempo de ejecución, porque el estándar exige al menos un guion en el nombre. La forma correcta es task-card.
  • Registrar el mismo nombre dos veces: si el código de definición del componente se ejecuta más de una vez (por ejemplo, por un error de configuración del bundler que duplica el módulo), el navegador lanzará un error indicando que ese nombre ya está definido. La solución habitual es asegurarse de que cada componente se define en un único fichero que se importa una sola vez.
  • Esperar que render() modifique el DOM directamente: render() no debe modificar el DOM a mano (por ejemplo, con document.querySelector); su única responsabilidad es devolver la plantilla mediante html. Es Lit quien se encarga de aplicar esa plantilla al DOM de forma eficiente.
  • Confundir la etiqueta HTML con el nombre de la clase: el nombre de la clase (TaskCard) puede ser cualquier identificador válido de JavaScript y no necesita guiones ni relación directa con el nombre de la etiqueta; es el primer argumento de customElements.define ('task-card') el que define cómo se usará en el HTML.

Ejercicios

  1. Crea un componente <user-avatar> que muestre, de forma completamente estática, el texto "AC" dentro de un <span> con un <div> alrededor (representando, de momento, las iniciales de un usuario fijo). Regístralo y úsalo en tu página de prueba.
  2. Añade una segunda instancia de <task-card> en el mismo index.html, justo debajo de la primera. Observa en el navegador que aparecen dos tarjetas idénticas, cada una como una instancia independiente del componente.
  3. Reescribe el componente task-card.js del apartado 5 usando el decorador @customElement en lugar de customElements.define, suponiendo que tu proyecto tiene soporte de decoradores (la plantilla de Lit de Vite lo tiene).

Soluciones

import { LitElement, html } from 'lit';

class UserAvatar extends LitElement {
  render() {
    return html`
      <div>
        <span>AC</span>
      </div>
    `;
  }
}

customElements.define('user-avatar', UserAvatar);
<script type="module" src="/src/components/user-avatar.js"></script>
...
<user-avatar></user-avatar>
<task-card></task-card>
<task-card></task-card>

Al recargar la página se verán dos tarjetas con exactamente el mismo contenido ("Preparar la demo del sprint"), porque de momento el componente es estático y no recibe ningún dato distinto por instancia. Esta limitación es precisamente la que resolverán las propiedades reactivas en el módulo 3.

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

@customElement('task-card')
class TaskCard extends LitElement {
  render() {
    return html`
      <article>
        <h3>Preparar la demo del sprint</h3>
        <p>Estado: En curso</p>
        <p>Asignada a: Ana</p>
      </article>
    `;
  }
}

El comportamiento en el navegador es idéntico al de la versión con customElements.define; solo cambia el estilo de escritura.

Conclusión

En esta lección has creado tu primer componente Lit siguiendo los tres pasos que se repetirán durante todo el curso: heredar de LitElement, implementar render() con la función html, y registrar el componente con customElements.define (o su equivalente @customElement). Con esa base, has creado la primera versión de <task-card>, todavía completamente estática, sin propiedades ni datos dinámicos: siempre muestra la misma tarea de ejemplo.

Te habrás fijado en que, al inspeccionar el componente en el navegador, aparece un #shadow-root que Lit crea automáticamente. En la siguiente lección, "Anatomía de un Componente Lit", se analiza con detalle qué significa esto, cómo se estructura por dentro una clase que extiende LitElement, y se ofrece una primera panorámica del ciclo de vida de un componente, antes de dar el salto, en el módulo 2, a las plantillas reactivas que permitirán que <task-card> deje de ser estática.

Curso de Lit

Módulo 1: Introducción a Lit y Web Components

Módulo 2: Plantillas Reactivas y Renderizado

Módulo 3: Propiedades y Estado Reactivo

Módulo 4: Estilos en Componentes Lit

Módulo 5: Eventos y Comunicación entre Componentes

Módulo 6: Ciclo de Vida y Comportamiento Avanzado

Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas

Módulo 8: Integración, Interoperabilidad y Despliegue

Módulo 9: Pruebas y Buenas Prácticas

Módulo 10: Proyecto: Construyendo TaskFlow

© Copyright 2026. Todos los derechos reservados