¿Te imaginas poder empaquetar HTML, CSS y JavaScript en una caja hermética que funcione en cualquier proyecto sin romper estilos? Con Web Components y Shadow DOM eso es una realidad. En este tutorial vamos a montar un componente desde cero, ver su estilado interno y comunicarlo con el exterior.
Índice
- Qué es Shadow DOM
- Primer componente básico
- Estilado encapsulado
- Comunicación entre componentes
- Conclusión
Qué es Shadow DOM
El Shadow DOM permite crear un árbol DOM separado del documento principal. Ese árbol tiene su propio scope de estilos, evitando colisiones con el CSS global. Es la pieza clave para que los componentes sean realmente aislados.
Primer componente básico
Vamos a crear un elemento my-card que muestre un título y un cuerpo. Usaremos la API de customElements.define y una plantilla template.
<template id='card-template'>
<style>
:host {
display: block;
border: 1px solid #ccc;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h2 {
margin-top: 0;
font-size: 1.25rem;
}
</style>
<h2 id='title'><slot name='title'>Título predeterminado</slot></h2>
<div id='content'><slot></slot></div>
</template>
<script>
class MyCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('card-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('my-card', MyCard);
</script>
Con este código el componente ya está registrado y listo para usarse en cualquier página.
Estilado encapsulado
Los estilos definidos dentro del <style> del template se aplican **solo** al contenido del Shadow DOM. No afectan a elementos fuera del componente, y viceversa. Esto permite crear componentes con una apariencia consistente sin preocuparse por CSS global.
Si necesitas que el componente responda a variables CSS del documento raíz, puedes exponerlas mediante :host o :host-context:
:host {
background: var(--card-bg, white);
}
:host(:hover) {
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
De esta forma, el consumidor del componente puede sobrescribir --card-bg desde su hoja de estilos.
Comunicación entre componentes
Los componentes pueden intercambiar datos mediante atributos, propiedades o eventos personalizados. Veamos cómo emitir un evento cuando el usuario hace clic en el título.
class MyCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('card-template');
shadow.appendChild(template.content.cloneNode(true));
this._title = shadow.querySelector('#title');
}
connectedCallback() {
this._title.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('title-click', {
bubbles: true,
composed: true,
detail: { message: 'El título fue pulsado' }
}));
});
}
}
customElements.define('my-card', MyCard);
Ahora, desde el documento principal podemos escuchar el evento:
document.addEventListener('title-click', e => {
console.log(e.detail.message);
});
Esta técnica mantiene el encapsulado, pero permite que el componente participe en la lógica de la aplicación.
Conclusión
Has visto cómo crear un Web Component con Shadow DOM, estilizarlo de forma aislada y comunicarlo mediante eventos. Estas piezas son la base para construir interfaces modulares, mantenibles y escalables. La próxima vez que necesites un widget reutilizable, considera empaquetarlo como un componente nativo antes de buscar una librería externa.