tutoriales.com

Creando Componentes Web Interpolares: Uso de `::part` y `::slotted` en Shadow DOM

Este tutorial te guiará a través del estilizado de componentes web utilizando las potentes pseudoclases `::part` y `::slotted` en Shadow DOM. Descubre cómo mantener tus estilos encapsulados mientras permites la personalización externa de tus componentes, logrando una flexibilidad y reutilización sin precedentes.

Intermedio15 min de lectura8 views
Reportar error

El desarrollo de componentes web reutilizables es fundamental en la construcción de aplicaciones modernas. Sin embargo, estilizar estos componentes de manera que sean robustos, encapsulados y a la vez personalizables, presenta sus propios desafíos. Aquí es donde entran en juego ::part y ::slotted, dos poderosas pseudoclases CSS que trabajan en conjunto con Shadow DOM para ofrecer una solución elegante a este problema.

En este tutorial, exploraremos en profundidad cómo utilizar ::part y ::slotted para estilizar tus componentes web, permitiendo a los desarrolladores que consumen tus componentes una forma controlada de personalizarlos sin romper el encapsulamiento del Shadow DOM. ¡Prepárate para llevar tus componentes web al siguiente nivel! 🚀


📖 ¿Qué es Shadow DOM y Por Qué es Crucial? ✨

Antes de sumergirnos en ::part y ::slotted, es vital entender el concepto de Shadow DOM. El Shadow DOM es una de las tres especificaciones clave de los Web Components (junto con Custom Elements y HTML Templates). Su función principal es proporcionar un árbol DOM encapsulado y aislado del DOM principal del documento.

🛡️ El Encapsulamiento del Shadow DOM

El encapsulamiento que ofrece Shadow DOM tiene dos grandes ventajas:

  1. Aislamiento de CSS: Los estilos definidos dentro de un Shadow DOM no se filtran al DOM principal, y viceversa. Esto evita conflictos de estilos y permite que los componentes sean verdaderamente autónomos.
  2. Aislamiento de JavaScript: El JavaScript dentro del Shadow DOM opera en su propio contexto, lo que previene manipulaciones accidentales del DOM principal y asegura que el componente funcione de forma independiente.
💡 Consejo: Piensa en Shadow DOM como una "caja negra" dentro de tu HTML, donde todo lo interno (estructura, estilos, comportamiento) está protegido del exterior a menos que tú decidas exponerlo explícitamente.

🎨 Estilizando Componentes en Shadow DOM: El Dilema

El fuerte encapsulamiento del Shadow DOM, aunque beneficioso, puede plantear un problema: ¿cómo permitimos a los usuarios de nuestro componente (desarrolladores) aplicar estilos personalizados a ciertas partes internas del mismo sin romper el encapsulamiento o forzarles a usar APIs de JavaScript complejas?

Aquí es donde ::part y ::slotted brillan, ofreciendo mecanismos controlados para "abrir" el Shadow DOM a la personalización de estilos.


🎯 Entendiendo ::part: Personalización por Partes Internas

La pseudoclase ::part() permite a un componente web exponer partes de su Shadow DOM para que puedan ser estilizadas desde fuera del Shadow DOM. Es como una "puerta trasera" controlada que el autor del componente abre para permitir la personalización.

✅ Cómo Funciona ::part

Para usar ::part:

  1. Define las partes en el componente: Dentro de la plantilla del Shadow DOM de tu componente, añade el atributo part="nombre-de-la-parte" a los elementos HTML internos que deseas exponer.
  2. Estiliza las partes desde fuera: En la hoja de estilos global o en el CSS de un componente padre, puedes usar nombre-del-custom-element::part(nombre-de-la-parte) para aplicar estilos a esas partes.
📌 Nota: Un elemento puede tener múltiples nombres de partes, separados por espacios, por ejemplo, `part="boton primario"`.

📝 Ejemplo Práctico: Un Botón Personalizable

Vamos a crear un componente de botón simple que permita la personalización de su texto y de su ícono interno.

<!-- index.html -->
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ejemplo de ::part y ::slotted</title>
    <style>
        body {
            font-family: sans-serif;
            margin: 20px;
            background-color: #f4f7f6;
        }

        my-button {
            display: block;
            margin-bottom: 15px;
        }

        /* Estilos personalizados para el componente my-button */
        my-button::part(texto-boton) {
            color: white;
            font-weight: bold;
            text-transform: uppercase;
            letter-spacing: 1px;
        }

        my-button::part(icono-boton) {
            color: yellow;
            margin-right: 5px;
            font-size: 1.2em;
        }

        /* Variante para un botón secundario */
        my-button.secundario {
            --color-base: #6c757d;
            --color-hover: #5a6268;
        }

        my-button.secundario::part(texto-boton) {
            color: #f8f9fa;
        }

        my-button.secundario::part(icono-boton) {
            color: lightblue;
        }
    </style>
</head>
<body>
    <h1>Componentes con Estilos Personalizables</h1>

    <h2>Botón Primario</h2>
    <my-button icon="⭐">Hacer clic aquí</my-button>

    <h2>Botón Secundario</h2>
    <my-button class="secundario" icon="👉">Más información</my-button>

    <script src="my-button.js"></script>
</body>
</html>
// my-button.js
class MyButton extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.render();
    }

    static get observedAttributes() {
        return ['icon'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === 'icon') {
            this.render();
        }
    }

    render() {
        const icon = this.getAttribute('icon') || '';
        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    display: inline-flex;
                    align-items: center;
                    gap: 8px;
                    padding: 10px 20px;
                    border: none;
                    border-radius: 5px;
                    cursor: pointer;
                    font-size: 16px;
                    transition: background-color 0.3s ease;
                    background-color: var(--color-base, #007bff); /* Variable CSS para el color base */
                    color: white;
                }

                :host(:hover) {
                    background-color: var(--color-hover, #0056b3); /* Variable CSS para el color hover */
                }

                .icon {
                    /* Estilos internos para el icono, puede ser sobrescrito por ::part */
                    font-size: 1em;
                }

                .text {
                    /* Estilos internos para el texto, puede ser sobrescrito por ::part */
                }
            </style>
            ${icon ? `<span class="icon" part="icono-boton">${icon}</span>` : ''}
            <span class="text" part="texto-boton"><slot></slot></span>
        `;
    }
}

customElements.define('my-button', MyButton);

En este ejemplo:

  • El <span> que contiene el ícono tiene part="icono-boton".
  • El <span> que contiene el texto del botón (proveniente del slot) tiene part="texto-boton".
  • En index.html, podemos estilizar directamente my-button::part(texto-boton) y my-button::part(icono-boton) para cambiar su apariencia desde fuera del Shadow DOM. Además, usamos variables CSS (--color-base, --color-hover) para permitir una personalización de color base sin necesidad de ::part, mostrando una técnica complementaria.
60% de entendimiento de ::part

💡 La Pseudoclase ::slotted(): Estilizando Contenido Proyectado

La pseudoclase ::slotted() se utiliza para estilizar los elementos que han sido proyectados dentro de un slot en el Shadow DOM. Es crucial entender que ::slotted() solo puede estilizar el elemento de nivel superior que se inserta en el slot, no sus descendientes internos.

🧩 Cómo Funciona ::slotted

  1. Define slots en el componente: En la plantilla del Shadow DOM, usa la etiqueta <slot> para definir puntos de inserción para el contenido externo.
  2. Inserta contenido externo: Al usar el componente, coloca contenido HTML dentro de las etiquetas del custom element.
  3. Estiliza el contenido proyectado: Dentro del style del Shadow DOM (o en un <style> global para el elemento host), usa ::slotted(selector) para aplicar estilos al contenido que ha sido insertado.
⚠️ Advertencia: `::slotted()` solo puede aplicar estilos a los nodos hijos directos de un ``. No puede "ver" o estilizar los descendientes anidados de esos nodos. Para estilizar los descendientes, esos descendientes necesitarían ser custom elements con sus propias partes (`::part`) o slots.

🖼️ Ejemplo Práctico: Una Tarjeta con Contenido Variable

Crearemos un componente <my-card> que puede contener un título y un párrafo, ambos insertados vía slot, y mostraremos cómo ::slotted puede estilizar esos elementos de nivel superior.

<!-- index.html -->
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ejemplo de ::part y ::slotted</title>
    <style>
        body {
            font-family: sans-serif;
            margin: 20px;
            background-color: #f4f7f6;
        }

        my-card {
            display: block;
            margin-bottom: 20px;
        }

        /* Estilos para el título proyectado */
        /* NO se aplica si el h2 tiene estilos en su propia hoja global, ya que ::slotted opera desde el shadow DOM */
        /* my-card h2 { color: #d63384; border-bottom: 1px solid #d63384; padding-bottom: 5px; } */

        /* Estilos generales para el texto en el DOM principal */
        p {
            line-height: 1.6;
            color: #343a40;
        }
    </style>
</head>
<body>
    <h1>Componentes con Estilos Personalizables</h1>

    <h2>Tarjeta de Información</h2>
    <my-card>
        <h2>Título de la Tarjeta</h2>
        <p>Este es el contenido principal de la tarjeta. Puede ser cualquier elemento HTML.</p>
        <ul>
            <li>Item 1</li>
            <li>Item 2</li>
        </ul>
    </my-card>

    <h2>Otra Tarjeta Diferente</h2>
    <my-card class="dark">
        <h3>Título Más Pequeño</h3>
        <p>Otra tarjeta con <span style="font-weight: bold; color: yellow;">texto importante</span> dentro.</p>
    </my-card>

    <script src="my-card.js"></script>
</body>
</html>
// my-card.js
class MyCard extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.render();
    }

    render() {
        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    display: block;
                    background-color: var(--card-bg, #ffffff);
                    border: 1px solid var(--card-border, #e0e0e0);
                    border-radius: 8px;
                    padding: 20px;
                    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
                    transition: transform 0.2s ease-in-out;
                }

                :host(:hover) {
                    transform: translateY(-3px);
                }

                /* Estilos que afectan a los elementos directamente insertados en el slot */
                ::slotted(h2) {
                    color: var(--heading-color, #007bff);
                    margin-top: 0;
                    margin-bottom: 15px;
                    border-bottom: 2px solid var(--heading-border, #007bff);
                    padding-bottom: 10px;
                }

                ::slotted(h3) {
                    color: var(--heading-color, #28a745);
                    margin-top: 0;
                    margin-bottom: 12px;
                    border-bottom: 1px solid var(--heading-border, #28a745);
                    padding-bottom: 8px;
                }

                ::slotted(p) {
                    color: var(--paragraph-color, #333);
                    line-height: 1.6;
                    margin-bottom: 10px;
                }

                ::slotted(ul) {
                    list-style-type: square;
                    margin-left: 20px;
                    color: var(--list-color, #555);
                }

                /* Estilos para el host cuando tiene la clase 'dark' */
                :host(.dark) {
                    --card-bg: #343a40;
                    --card-border: #495057;
                    --heading-color: #ffc107;
                    --heading-border: #ffc107;
                    --paragraph-color: #f8f9fa;
                    --list-color: #e9ecef;
                }
            </style>
            <slot></slot>
        `;
    }
}

customElements.define('my-card', MyCard);

En este ejemplo:

  • El <my-card> acepta cualquier contenido HTML a través de un slot por defecto.
  • Dentro del Shadow DOM de <my-card>, usamos ::slotted(h2), ::slotted(h3), ::slotted(p), y ::slotted(ul) para aplicar estilos a los <H2>, <H3>, <P> y <UL> que se insertan en el componente.
  • Observa que los estilos como color: yellow; en el <span> dentro del párrafo de la segunda tarjeta no son afectados por ::slotted(p) porque ::slotted solo afecta al elemento directamente proyectado (<p>), no a sus descendientes. Para estilizar ese <span>, se necesitaría definir un part en el <span> dentro del p, si <p> fuera parte del Shadow DOM, o estilizarlo directamente en el DOM principal si es contenido del DOM principal (como es el caso aquí).
  • También usamos variables CSS en el :host para permitir una personalización de temas (clase dark) que afecte a todos los elementos internos y slotted de forma coordinada.

🔄 ::part vs ::slotted: ¿Cuándo Usar Cuál?

Aunque ambos permiten la personalización de estilos, ::part y ::slotted abordan problemas ligeramente diferentes y operan bajo reglas distintas.

Característica::part::slotted()
---------
PropósitoEstilizar partes internas del Shadow DOM expuestas por el autor del componente.Estilizar elementos proyectados (slot) en el Shadow DOM desde dentro del Shadow DOM.
AlcancePuede estilizar cualquier elemento al que el autor del componente le haya añadido el atributo part.Solo estiliza los hijos directos del <slot> al que se aplica.
---------
Origen del EstiloEl estilo se aplica desde fuera del Shadow DOM, usando el selector nombre-del-elemento::part(nombre-de-la-parte).El estilo se aplica desde dentro del Shadow DOM, usando el selector ::slotted(selector-de-elemento).
EncapsulamientoRompe el encapsulamiento de estilos para las partes específicas de forma controlada.Mantiene el encapsulamiento del Shadow DOM, estilizando el contenido externo desde la perspectiva del componente.
---------
FlexibilidadIdeal para un control preciso sobre la apariencia de elementos internos fijos del componente.Ideal para adaptar el estilo de contenido arbitrario (pero de nivel superior) que se inyecta en el componente.
Ejemplo de UsoCambiar el color de un botón interno, fuente de un encabezado interno.Establecer márgenes para un <h2> proyectado, color de un <p> proyectado.
DOM Principal CSS Global ::part(accion) { ... } <mi-componente> #shadow-root (open) Botón Interno part="accion" <slot> Contenido Externo <p>Texto</p> Estilo vía ::part Estilo vía ::slotted(p) ::slotted(p) { ... }

🛠️ Buenas Prácticas y Consideraciones Avanzadas

📦 Uso de Variables CSS con ::part y ::slotted

Combinar ::part y ::slotted con variables CSS (custom properties) es una estrategia extremadamente potente para maximizar la flexibilidad y personalización. Puedes definir variables CSS dentro de tu Shadow DOM y usarlas como valores por defecto, permitiendo que sean sobrescritas desde el exterior.

💡 Consejo: Define variables CSS en `:host` para que puedan ser fácilmente sobrescritas por el consumidor del componente, y úsalas en tus estilos internos y en los estilos de `::slotted`.

Ejemplo de Variables CSS:

/* Dentro del Shadow DOM de my-component */
:host {
    --primary-color: #007bff;
    --text-color: #333;
}

.header {
    background-color: var(--primary-color);
    color: var(--text-color);
}

::slotted(p) {
    color: var(--text-color);
}

/* Desde el CSS global, para un custom element específico */
my-component {
    --primary-color: #28a745;
    --text-color: #fff;
}

🚀 Encapsulamiento Inteligente

No expongas todas las partes de tu componente. Solo expón aquellas que razonablemente esperas que los desarrolladores necesiten personalizar. Demasiadas partes pueden hacer que tu componente sea difícil de mantener y que su API de estilos sea inconsistente.

🔗 Estilos en Cadena con ::part

Si tienes un custom element anidado dentro de otro custom element, y ambos exponen partes, puedes encadenar los selectores ::part.

<!-- Uso -->
<my-app>
    <my-widget></my-widget>
</my-app>
<!-- my-app.js Shadow DOM -->
<div part="contenedor-principal">
    <my-widget part="widget-interno"></my-widget>
</div>
<!-- my-widget.js Shadow DOM -->
<div part="cabecera-widget">...</div>
/* Estilo global */
my-app::part(widget-interno)::part(cabecera-widget) {
    background-color: orange;
}

Este encadenamiento permite una gran granularidad en el control de estilos, pero úsalo con moderación para evitar selectores excesivamente complejos.

⚠️ Limitaciones de ::slotted()

Recuerda la limitación clave: ::slotted() solo afecta a los elementos hijos directos de un <slot>. Si necesitas estilizar elementos anidados dentro del contenido proyectado, esos elementos anidados tendrían que ser ellos mismos custom elements con sus propias parts, o tendrías que aplicar los estilos al contenido proyectado desde el DOM principal (lo cual anularía el encapsulamiento para esa porción).

¿Por qué ::slotted() tiene esta limitación? Esta limitación existe para preservar el encapsulamiento del Shadow DOM. Si `::slotted()` pudiera estilizar descendientes arbitrarios del contenido proyectado, se rompería el aislamiento y se crearían dependencias internas que harían el componente más frágil y difícil de mantener. La intención es que el *consumidor* del componente controle el estilo de su propio contenido, y el *componente* controle el estilo de sus propios elementos internos y cómo se muestra el contenido proyectado de nivel superior.

Buenas Prácticas

  • Nombres de partes descriptivos: Usa nombres de partes claros y significativos (part="boton-primario", part="cabecera-tarjeta").
  • Documentación: Siempre documenta qué partes expone tu componente y qué variables CSS acepta. Esto es crucial para los desarrolladores que usarán tu componente.
  • Prioridad de estilos: Recuerda que los estilos aplicados vía ::part desde el DOM global tienen mayor especificidad que los estilos internos del Shadow DOM (a menos que los internos usen !important o selectores más específicos, lo cual se debe evitar).

🏁 Conclusión: El Poder de la Personalización Controlada

::part y ::slotted son herramientas indispensables para cualquier desarrollador que trabaje con Web Components. Permiten un equilibrio perfecto entre encapsulamiento y flexibilidad, empoderando a los autores de componentes para crear elementos reutilizables y robustos, mientras que ofrecen a los consumidores la capacidad de personalizar la apariencia para que se adapte a sus necesidades de diseño.

Al dominar estas pseudoclases, no solo crearás componentes más versátiles, sino que también contribuirás a un ecosistema de desarrollo web más modular y mantenible. ¡Sigue experimentando y construyendo! 🎉

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!