Shadow DOM: Slots y Composición
Aprende slots y composición en el Shadow DOM de JavaScript: slots por defecto y con nombre, light DOM vs. shadow DOM, el árbol aplanado, el evento slotchange y assignedNodes/assignedElements.
Los slots y la composición son lo que hace que el Shadow DOM sea genuinamente reutilizable. El autor de un componente escribe una estructura interna fija una sola vez, y los consumidores la rellenan con su propio marcado — sin que ambos colisionen jamás. Esta página cubre el elemento <slot> (slots por defecto y con nombre), cómo el light DOM y el shadow DOM se combinan en un árbol aplanado, el evento slotchange, y los métodos assignedNodes() / assignedElements() que se usan para leer qué contenido llegó a un slot.
Si eres nuevo en Shadow DOM, lee primero Shadow DOM para los conceptos básicos de attachShadow() y las raíces shadow, y Web Components para entender cómo los slots encajan junto a los elementos personalizados y las plantillas.
Light DOM vs. shadow DOM
La composición implica dos árboles:
- Light DOM — el marcado que el usuario escribe entre las etiquetas de tu elemento:
<my-card>...esta parte...</my-card>. Vive en el documento normal y permanece allí. - Shadow DOM — el marcado que tú adjuntas con
attachShadow(). Está encapsulado y no es directamente accesible desde el documento exterior.
Un <slot> es una ventana: reside en el shadow DOM y proyecta los hijos del light DOM dentro de él. Los nodos del light DOM no se mueven — solo se muestran en la posición del slot. Esta vista combinada es el árbol aplanado, y es lo que el navegador realmente renderiza y estiliza.
Entendiendo los slots en Shadow DOM
Un slot es un marcador de posición en tu shadow DOM donde el navegador coloca el contenido suministrado desde el light DOM. Los slots son la manera en que un componente genérico permite que cada instancia luzca diferente mientras comparte una misma plantilla interna.
Definir un slot por defecto
Usa el elemento <slot>. Un slot sin atributo name es el slot por defecto: captura cualquier hijo del light DOM que no tenga atributo slot. El texto dentro de <slot> es contenido de reserva, que se muestra solo cuando no se le asigna nada.
<body>
<script>
class CustomElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<div class="container">
<slot>Default content</slot>
</div>
`;
}
}
customElements.define('custom-element', CustomElement);
</script>
<!-- No children: the slot shows its fallback, "Default content" -->
<custom-element></custom-element>
<!-- Children with no slot attribute go into the default slot -->
<custom-element><strong>Hello from the light DOM!</strong></custom-element>
</body>El primer <custom-element> muestra "Default content" porque no se le asignó nada. El segundo muestra el texto en negrita — su hijo del light DOM reemplaza el contenido de reserva. Ten en cuenta que el marcado sigue viviendo en el documento; el slot solo lo visualiza.
Slots con nombre
Cuando un componente tiene más de un punto de inserción, asigna un name a cada <slot> y combínalo desde el light DOM con un atributo slot="...". Así es como se dirige el contenido correcto al lugar correcto.
<body>
<!-- Define Custom Element -->
<script>
// Define Custom Element Class
class CustomElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
/* Define styles for the component */
.container {
border: 1px solid #ccc;
padding: 20px;
}
</style>
<div class="container">
<slot name="content">Default content</slot>
</div>
`;
}
}
// Define Custom Element
customElements.define('custom-element', CustomElement);
</script>
<!-- Displaying the custom element -->
<custom-element>
<div slot="content">Content from parent</div>
</custom-element>
</body>El <div slot="content"> se empareja con <slot name="content">, por lo que "Content from parent" reemplaza el contenido de reserva. Cualquier contenido que no coincida con un slot con nombre caerá en el slot por defecto, si existe, o simplemente no se renderizará.
Mejorando la composición en Shadow DOM
La composición en el contexto del Shadow DOM se refiere a ensamblar componentes de interfaz de usuario y contenido combinando slots y su contenido distribuido para crear estructuras más complejas y reutilizables. Cuando se aplica en el contexto del Shadow DOM, la composición permite crear componentes web altamente personalizables y reutilizables.
Para estilizar el contenido distribuido en los slots desde el padre, usa el pseudoelemento CSS ::slotted() — por ejemplo, ::slotted(div) { color: blue; }. Consulta Shadow DOM Styling para ver el panorama completo de ::slotted(), :host y las propiedades personalizadas de CSS.
Componer componentes con slots
Una forma poderosa de aprovechar la composición es combinar múltiples slots en un diseño estructurado. Aquí un componente compuesto define regiones de cabecera, contenido y pie de página:
<body>
<script>
// Define Composite Element Class
class CompositeElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
/* Define styles for the composite component */
.container {
border: 1px solid #ccc;
padding: 20px;
}
</style>
<div class="container">
<slot name="header"></slot>
<slot name="content"></slot>
<slot name="footer"></slot>
</div>
`;
}
}
// Define Composite Element
customElements.define('composite-element', CompositeElement);
</script>
<composite-element>
<div slot="header">Header</div>
<div slot="content">Content</div>
<div slot="footer">Footer</div>
</composite-element>
</body>Cada hijo con slot="..." se dirige a su slot con nombre correspondiente, produciendo un diseño limpio de cabecera/contenido/pie de página que cualquier instancia puede rellenar de forma diferente.
Reaccionar a los cambios de slot con slotchange
El contenido en los slots es dinámico: un consumidor puede agregar, eliminar o reemplazar hijos del light DOM en cualquier momento. El evento slotchange se dispara en un <slot> cada vez que cambian sus nodos asignados, de modo que tu componente puede reaccionar — volver a renderizar un resumen, validar, cargar de forma diferida, etc. Escúchalo desde dentro de la raíz shadow:
<body>
<script>
class TabList extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = `<p id="count"></p><slot></slot>`;
this._slot = root.querySelector('slot');
this._count = root.querySelector('#count');
}
connectedCallback() {
this._slot.addEventListener('slotchange', () => this.update());
this.update();
}
update() {
// assignedElements() returns only element nodes in the slot
const items = this._slot.assignedElements();
this._count.textContent = `Tabs: ${items.length}`;
}
}
customElements.define('tab-list', TabList);
</script>
<tab-list>
<button>One</button>
<button>Two</button>
</tab-list>
<script>
// Adding a child later fires slotchange → count updates to 3
const list = document.querySelector('tab-list');
const extra = document.createElement('button');
extra.textContent = 'Three';
list.appendChild(extra);
</script>
</body>Inicialmente el componente muestra "Tabs: 2". Cuando se añade el tercer <button>, se dispara slotchange y el contador se actualiza a "Tabs: 3".
Leer el contenido en slots: assignedNodes() vs. assignedElements()
Ambos métodos se invocan en un <slot> y devuelven lo que el navegador le asignó desde el light DOM:
slot.assignedNodes()devuelve todos los nodos — elementos y nodos de texto (incluidos los espacios en blanco entre etiquetas).slot.assignedElements()devuelve únicamente los nodos de tipo elemento. Esto es generalmente lo que necesitas.
Pasa { flatten: true } para descender en slots anidados cuando los slots están encadenados entre componentes:
// All nodes, including stray text/whitespace nodes
slot.assignedNodes(); // e.g. [text, <button>, text, <button>, text]
// Elements only — cleaner for iteration
slot.assignedElements(); // e.g. [<button>, <button>]
// Flatten through nested <slot> assignments
slot.assignedElements({ flatten: true });Prefiere assignedElements() a menos que necesites específicamente los nodos de texto; te evita tener que filtrar los espacios en blanco.
El árbol aplanado, en resumen
El navegador no mueve literalmente los nodos del light DOM al shadow DOM. En cambio, construye un árbol aplanado sustituyendo cada slot por sus nodos asignados para el renderizado y el estilizado. Consecuencias prácticas:
- Los elementos en slots permanecen en el documento, por lo que
document.querySelector()sigue encontrándolos y susclass/idoriginales siguen aplicándose. - Están estilizados por el CSS de la página, mientras que el componente solo los alcanza mediante
::slotted(). - Los escuchadores de eventos adjuntos en el light DOM siguen funcionando — los eventos burbujean a través del árbol aplanado.
Conclusión
Los slots y la composición convierten un shadow DOM encapsulado en un componente flexible y reutilizable: tú defines la estructura, y los consumidores suministran el contenido a través de slots por defecto y con nombre. Recuerda las piezas clave — light DOM vs. shadow DOM, el árbol aplanado que renderiza el navegador, el evento slotchange para reaccionar a los cambios, y assignedElements() para leer qué se colocó en el slot.
Para profundizar, consulta Web Components para una visión más amplia, Custom Elements para el ciclo de vida de los elementos, Shadow DOM Styling para ::slotted() y :host, y Shadow DOM para los fundamentos.