template, slot y Web Components
En esta lección
Cuando una web crece, empiezas a repetir los mismos trozos de HTML: la tarjeta de un producto, un aviso, una fila de una tabla. Copiar y pegar funciona hasta que tienes que cambiar algo en veinte sitios. En esta lección verás tres herramientas que trae el propio navegador para reutilizar HTML: el elemento <template>, las etiquetas propias (custom elements) y el shadow DOM con <slot>. Juntas forman lo que se llama Web Components. Necesitarás un poco de JavaScript; si todavía no lo has estudiado, quédate con las ideas y vuelve después de la lección sobre el DOM.
<template>: HTML que espera su turno
<template> guarda un trozo de HTML que no se muestra ni se ejecuta al cargar la página. Es un molde: está ahí para que JavaScript haga copias cuando las necesite.
<template id="plantilla-ruta">
<li class="ruta">
<strong class="nombre"></strong>
<span class="km"></span>
</li>
</template>
Lo que hay dentro de un <template> es inerte:
- No se ve en la página.
- Sus imágenes no se descargan y sus
<script>no se ejecutan. document.querySelector('.ruta')no lo encuentra, porque no forma parte del documento visible.
Puedes poner un <template> en casi cualquier sitio: en el <head>, en el <body> o incluso dentro de una tabla, donde un <div> no estaría permitido.
Usar la plantilla
El contenido del molde está en la propiedad content. Para usarlo, se clona, se rellena y se añade a la página:
const rutas = [
{ nombre: 'Senda costera', km: 8 },
{ nombre: 'Bosque de hayas', km: 6 },
];
const plantilla = document.querySelector('#plantilla-ruta');
const lista = document.querySelector('#lista');
for (const ruta of rutas) {
const copia = plantilla.content.cloneNode(true);
copia.querySelector('.nombre').textContent = ruta.nombre;
copia.querySelector('.km').textContent = ` · ${ruta.km} km`;
lista.append(copia);
}
cloneNode(true)hace una copia profunda: el elemento y todo lo que tiene dentro. Confalsesolo copiaría el<li>vacío.- Cada vuelta del bucle crea una copia nueva; la plantilla original queda intacta para la siguiente.
¿Por qué no construirlo con una cadena de texto?
Podrías escribir lista.innerHTML += '<li>' + ruta.nombre + '</li>'. Tiene dos problemas:
- Seguridad. Si
ruta.nombreviene de un usuario y contiene algo como<img src=x onerror="...">, ese código se ejecutará en tu página. Es un ataque conocido como XSS (cross-site scripting). Con la plantilla ytextContent, el texto se trata siempre como texto. - Orden. El HTML queda en el HTML, donde tu editor lo colorea y el validador lo revisa, en lugar de estar troceado entre comillas dentro de JavaScript.
Esto lo practicarás más en modificar el DOM.
Web Components: tus propias etiquetas
HTML tiene unas cien etiquetas. ¿Y si quieres una <tarjeta-aviso> o un <precio-euros> que se comporten siempre igual en todas tus páginas? Los Web Components son un conjunto de estándares que lo permiten sin ninguna librería. Se apoyan en tres piezas:
| Pieza | Para qué |
|---|---|
| Custom elements | Crear etiquetas nuevas con su propio comportamiento |
| Shadow DOM | Darles un interior privado, con estilos que no se mezclan con los de la página |
<template> y <slot> | Definir su estructura y dónde va el contenido que les pasas |
Funcionan en todos los navegadores actuales y se pueden usar en cualquier web, con o sin frameworks.
Custom elements
Reglas del nombre
Una etiqueta propia:
- Debe llevar al menos un guion:
tarjeta-aviso,mapa-ruta,precio-euros. Así nunca chocará con una etiqueta oficial, que nunca llevan guion. - Debe ir en minúsculas y empezar por una letra.
- Siempre se cierra:
<precio-euros></precio-euros>. No existe<precio-euros />.
Si intentas registrar un nombre sin guion, como tarjeta, JavaScript lanza un error de tipo SyntaxError.
Definir el comportamiento
Un custom element es una clase de JavaScript que hereda de HTMLElement y se registra con customElements.define(). (Las clases las verás en clases y POO; aquí basta con copiar el patrón.)
<p>Cuota mensual: <precio-euros cantidad="39.9"></precio-euros></p>
class PrecioEuros extends HTMLElement {
static observedAttributes = ['cantidad'];
attributeChangedCallback(nombre, anterior, nuevo) {
const formato = new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' });
this.textContent = formato.format(Number(nuevo));
}
}
customElements.define('precio-euros', PrecioEuros);
La página mostrará Cuota mensual: 39,90 €. Y si más tarde otro script cambia el atributo con setAttribute('cantidad', '45'), el texto pasará a 45,00 € sin hacer nada más.
static observedAttributeses la lista de atributos que el componente vigila.attributeChangedCallbackse ejecuta cada vez que uno de ellos cambia, también la primera vez, al crear el elemento. Recibe el nombre del atributo, el valor anterior y el nuevo.
El ciclo de vida
El navegador llama a ciertos métodos de la clase en momentos concretos. Se conocen como callbacks del ciclo de vida:
| Método | Cuándo se ejecuta | Para qué se usa |
|---|---|---|
constructor() | Al crear el elemento | Preparar el estado y el shadow DOM. Debe empezar con super() |
connectedCallback() | Al añadirlo a la página | Pintar el contenido, escuchar eventos |
disconnectedCallback() | Al quitarlo de la página | Limpiar: parar temporizadores, quitar escuchadores |
attributeChangedCallback() | Al cambiar un atributo vigilado | Actualizar lo que se ve |
Antes de que llegue el JavaScript
Si el script carga tarde, durante un instante el navegador ve una etiqueta que no conoce. No da error: la trata como un elemento en línea sin estilos y muestra su contenido tal cual. Puedes ocultarla hasta que esté lista con la pseudoclase :defined:
tarjeta-aviso:not(:defined) {
visibility: hidden;
}
Shadow DOM: un interior privado
Imagina que tu componente tiene un <h4> y la página donde lo usas tiene la regla h4 { color: green; }. Sin protección, el título de tu componente se volvería verde. Y al revés: los estilos de tu componente podrían estropear la página.
El shadow DOM (DOM “en la sombra”) es un árbol de elementos escondido dentro del componente, con su propio CSS. Los selectores de fuera no entran y los de dentro no salen.
class TarjetaAviso extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host { display: block; border-left: 6px solid #1971c2;
background: #e7f5ff; padding: 8px 12px; }
:host([tipo="peligro"]) { border-color: #c92a2a; background: #fff5f5; }
h4 { margin: 0 0 4px; }
</style>
<h4><slot name="titulo">Aviso</slot></h4>
<slot></slot>
`;
}
}
customElements.define('tarjeta-aviso', TarjetaAviso);
attachShadow({ mode: 'open' })crea el árbol privado. Conopen, se puede acceder a él desde fuera conelemento.shadowRoot(útil para depurar). Es la opción habitual.- El
<style>de dentro solo afecta al interior del componente. Eseh4 { margin: 0 }no toca los<h4>de la página. :hostselecciona al propio componente desde dentro. Por defecto, una etiqueta propia es en línea (inline); por eso conviene darledisplay: block.:host([tipo="peligro"])aplica estilos cuando el componente tiene ese atributo.
En las DevTools, en la pestaña Elementos, verás el interior bajo una línea que pone #shadow-root (open).
Lo que sí atraviesa la sombra
El aislamiento no es total, y es a propósito:
- Las propiedades que se heredan, como
color,font-familyoline-height, sí pasan de la página al componente. Así encaja con la tipografía de tu web. - Las variables CSS también atraviesan la sombra. Es la forma recomendada de dejar que la página personalice un componente: dentro usas
var(--color-aviso, #1971c2)y fuera defines--color-aviso.
<slot>: huecos para tu contenido
Un componente no sirve de mucho si siempre muestra lo mismo. Un slot es un hueco dentro del shadow DOM donde se coloca el contenido que escribes entre las etiquetas del componente:
<tarjeta-aviso tipo="peligro">
<span slot="titulo">Cierre por lluvia</span>
La ruta del Pico del Cueto está cerrada este sábado.
</tarjeta-aviso>
- El
<span slot="titulo">va al<slot name="titulo">: es un slot con nombre. - Todo lo que no lleva atributo
slot(aquí, la frase) va al<slot>sin nombre: el slot por defecto. - Lo que escribes dentro de un
<slot>en el componente (Aviso) es el contenido de reserva: se muestra si no le pasas nada a ese hueco.
El contenido que pasas sigue perteneciendo a tu página: los buscadores lo leen, tus estilos le afectan y JavaScript lo encuentra con querySelector. El componente solo decide dónde se coloca. Desde dentro, puedes darle algún estilo básico con el selector ::slotted(), por ejemplo ::slotted(a) { font-weight: bold; }.
Plantilla y componente juntos
En lugar de escribir el interior en una cadena de texto, el componente puede tomarlo de un <template> de la página. Es más cómodo cuando el HTML es largo:
<template id="molde-ficha">
<style>
:host { display: block; border: 1px solid #ccc; padding: 12px; }
</style>
<h3><slot name="nombre">Sin nombre</slot></h3>
<p><slot></slot></p>
</template>
<ficha-socio>
<span slot="nombre">Lucía Gómez</span>
Socia desde 2024. Nivel: intermedio.
</ficha-socio>
class FichaSocio extends HTMLElement {
constructor() {
super();
const molde = document.querySelector('#molde-ficha');
this.attachShadow({ mode: 'open' }).append(molde.content.cloneNode(true));
}
}
customElements.define('ficha-socio', FichaSocio);
attachShadow() devuelve la raíz del shadow DOM, así que se puede encadenar con append().
Shadow DOM sin JavaScript
También existe una forma de declarar el shadow DOM directamente en el HTML, con el atributo shadowrootmode en un <template>:
<ficha-socio>
<template shadowrootmode="open">
<style>p { color: #555; }</style>
<p><slot></slot></p>
</template>
Marcos Ruiz, socio desde 2025.
</ficha-socio>
El navegador convierte ese <template> en el shadow DOM del elemento al leer la página. Se usa sobre todo cuando el HTML lo genera un servidor. Solo funciona al cargar la página: si lo insertas después con innerHTML, no se activa.
¿Cuándo usar Web Components?
- Sí: piezas que reutilizas en muchas páginas o proyectos (un selector de fechas, un mapa, un reproductor), o widgets que quieres poder pegar en webs hechas con tecnologías distintas.
- Quizá no: una web pequeña con dos tarjetas. Un poco de CSS y una plantilla bastan.
Frameworks como React, Vue o Astro (con el que está hecha esta web) también trabajan con componentes, cada uno a su manera. Lo que aprendes aquí te servirá para entenderlos, porque la idea es la misma: piezas con su estructura, su aspecto y su comportamiento, que se combinan para construir páginas.
Cuidado: una etiqueta propia no tiene significado para los lectores de pantalla ni para el teclado. Si tu componente es un botón, pon un
<button>de verdad dentro. Repasa accesibilidad antes de crear controles propios.
Errores frecuentes
- Nombre sin guion o con mayúsculas.
customElements.define('tarjeta', ...)lanza un error. Usa siempre algo comotarjeta-aviso. - Cerrar la etiqueta con
/>. Las etiquetas propias necesitan cierre explícito:<mi-mapa></mi-mapa>. - Usar
plantillaen vez deplantilla.content. Lo que se clona es el contenido del<template>, no el elemento. - Olvidar
super()en el constructor. Es obligatorio antes de usarthis. - Llamar dos veces a
attachShadow(). Da error. Hazlo una sola vez, en el constructor. - Construir HTML con datos del usuario en
innerHTML. Riesgo de XSS. Usa plantillas ytextContent. - Esperar que los estilos de la página entren en el shadow DOM. No entran (salvo los heredados y las variables CSS). Para personalizar, usa variables.
Resumen
| Herramienta | Qué hace |
|---|---|
<template> | Guarda HTML inerte para clonarlo con JavaScript |
plantilla.content.cloneNode(true) | Copia el contenido de la plantilla |
customElements.define('mi-etiqueta', Clase) | Registra una etiqueta propia (con guion) |
connectedCallback, disconnectedCallback | Al añadir o quitar el componente de la página |
observedAttributes + attributeChangedCallback | Reaccionar a cambios de atributos |
attachShadow({ mode: 'open' }) | Crea un interior con estilos aislados |
:host | El componente visto desde su propio CSS |
<slot> y <slot name="..."> | Huecos donde va el contenido que le pasas |
<template shadowrootmode="open"> | Shadow DOM declarado en el HTML |
En la siguiente lección dibujarás con código: gráficos con SVG y canvas.
Pruébalo tú
Cambia el código y pulsa Ejecutar (o Ctrl + Enter).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.