Los Web Components son un conjunto de APIs nativas del navegador que permiten crear elementos HTML reutilizables y encapsulados sin depender de React, Vue ni ningún otro framework. Se basan en tres pilares: Custom Elements para definir nuevas etiquetas HTML, Shadow DOM para aislar estilos y estructura, y HTML Templates para definir marcado reutilizable con slots.
Custom Elements: definir nuevas etiquetas HTML
Con customElements.define registras una clase que extiende HTMLElement y la asocias a un nombre de etiqueta (que debe contener un guion). El ciclo de vida tiene cuatro callbacks: connectedCallback, disconnectedCallback, attributeChangedCallback y adoptedCallback.
class ContadorElemento extends HTMLElement {
static observedAttributes = ['valor-inicial'];
#cuenta = 0;
connectedCallback() {
this.#cuenta = parseInt(this.getAttribute('valor-inicial') ?? '0', 10);
this.#render();
this.addEventListener('click', this.#incrementar);
}
disconnectedCallback() {
this.removeEventListener('click', this.#incrementar);
}
attributeChangedCallback(nombre, antes, despues) {
if (nombre === 'valor-inicial') {
this.#cuenta = parseInt(despues, 10);
this.#render();
}
}
#incrementar = () => {
this.#cuenta++;
this.#render();
this.dispatchEvent(new CustomEvent('cambio', { detail: this.#cuenta, bubbles: true }));
};
#render() {
this.textContent = `Clics: ${this.#cuenta}`;
}
}
customElements.define('mi-contador', ContadorElemento);
// <mi-contador valor-inicial="5"></mi-contador>
Shadow DOM: encapsulación de estilos
El Shadow DOM crea un árbol DOM interno aislado del documento principal. Los estilos definidos dentro no afectan al exterior y los estilos del documento no entran al Shadow DOM, evitando colisiones de CSS.
class TarjetaProducto extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
font-family: sans-serif;
}
.nombre { font-size: 1.2em; font-weight: bold; color: #333; }
.precio { color: #e44; font-size: 1.4em; margin-top: 8px; }
button {
background: #0066cc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-top: 12px;
}
button:hover { background: #0052a3; }
</style>
<div class="nombre">${this.getAttribute('nombre')}</div>
<div class="precio">${this.getAttribute('precio')}</div>
<button>Añadir al carrito</button>
`;
shadow.querySelector('button').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('anadir-carrito', {
detail: { nombre: this.getAttribute('nombre') },
bubbles: true,
composed: true, // el evento cruza el Shadow DOM
}));
});
}
}
customElements.define('tarjeta-producto', TarjetaProducto);
HTML Templates con slots
El elemento <template> define marcado que no se renderiza hasta que se clona explícitamente. Los <slot> dentro del Shadow DOM son puntos de inserción para el contenido que el usuario pone entre las etiquetas del componente:
// Definir la plantilla en HTML:
// <template id="tarjeta-tpl">
// <style> ... </style>
// <div class="cabecera"><slot name="titulo">Sin título</slot></div>
// <div class="cuerpo"><slot></slot></div>
// </template>
class TarjetaConSlots extends HTMLElement {
connectedCallback() {
const tpl = document.getElementById('tarjeta-tpl');
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(tpl.content.cloneNode(true));
}
}
customElements.define('mi-tarjeta', TarjetaConSlots);
// Uso en HTML:
// <mi-tarjeta>
// <h2 slot="titulo">Título del artículo</h2>
// <p>Contenido que va al slot por defecto</p>
// </mi-tarjeta>
Componente reactivo con atributos observados
Un botón con tema configurable que reacciona a cambios de atributos, combinando Custom Elements y Shadow DOM:
class BotonTema extends HTMLElement {
static observedAttributes = ['tema', 'deshabilitado'];
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.#actualizar();
}
attributeChangedCallback() {
this.#actualizar();
}
#actualizar() {
if (!this.shadowRoot) return;
const tema = this.getAttribute('tema') || 'primario';
const colores = { primario: '#0066cc', peligro: '#cc0000', exito: '#007733' };
this.shadowRoot.innerHTML = `
<style>
button {
background: ${colores[tema] ?? colores.primario};
color: white; border: none; padding: 10px 20px;
border-radius: 4px; cursor: pointer;
opacity: ${this.hasAttribute('deshabilitado') ? 0.5 : 1};
}
</style>
<button ${this.hasAttribute('deshabilitado') ? 'disabled' : ''}>
<slot>Botón</slot>
</button>
`;
}
}
customElements.define('boton-tema', BotonTema);
// <boton-tema tema="peligro">Eliminar</boton-tema>
Los Web Components son especialmente útiles para equipos que mezclan tecnologías o necesitan componentes que funcionen en proyectos con distintos frameworks. Al ser estándar del navegador, un <mi-tarjeta> funciona igual en React, Vue, Angular o HTML plano.
