Saltar al contenido

Shadow DOM de JavaScript

El Shadow DOM es una característica clave de Web Components, que permite a los desarrolladores crear árboles DOM encapsulados y ámbitos de estilo. Esta guía está diseñada para proporcionar una comprensión profunda del Shadow DOM, junto con ejemplos prácticos de código para demostrar su uso.

¿Qué es Shadow DOM?

Shadow DOM permite a los desarrolladores encapsular una parte del DOM y sus estilos, aislándola del resto del documento. Esto garantiza que los estilos y scripts dentro del Shadow DOM no entren en conflicto con los del documento principal.


html
<head>
  <style>
    .shadow-box {
      padding: 10px;
      border: 1px solid #000;
      background-color: lightcoral;
      color: white;
    }
  </style>
</head>
<body>
  <div class="shadow-box">This is styled by the main document</div>
  <div id="host"></div>
  <script>
    // Create a shadow root
    const hostElement = document.getElementById('host');
    const shadowRoot = hostElement.attachShadow({ mode: 'open' });

    // Attach shadow DOM content
    shadowRoot.innerHTML = `
      <style>
        .shadow-box {
          padding: 10px;
          border: 1px solid #000;
          background-color: lightblue;
          color: black;
        }
      </style>
      <div class="shadow-box">Hello, Shadow DOM!</div>
    `;
  </script>
</body>

En este ejemplo, hay dos elementos con el mismo nombre de clase shadow-box. El primer elemento recibe estilos del CSS del documento principal, mientras que el segundo elemento recibe estilos del CSS del Shadow DOM. Como puedes ver, los estilos definidos en el Shadow DOM no afectan a los elementos del documento principal y viceversa. Esto demuestra el encapsulamiento que proporciona el Shadow DOM, permitiéndote crear componentes aislados y reutilizables sin preocuparte por conflictos de estilos.

Creación de un Shadow Root

Para crear un shadow root, usa el método attachShadow en un elemento. El shadow root puede ser open o closed. Un shadow root open es accesible desde JavaScript fuera del árbol shadow, mientras que un shadow root closed no lo es.

Shadow Root abierto

Un shadow root abierto permite acceso y manipulación desde JavaScript externo. En el siguiente ejemplo, manipulamos el contenido de texto dentro del shadow root después de que se crea.


html
<body>
  <div id="open-shadow-host"></div>
  <button id="open-shadow-btn">Change Shadow Content</button>

  <script>
    const openShadowHost = document.getElementById('open-shadow-host');
    const openShadowRoot = openShadowHost.attachShadow({ mode: 'open' });

    openShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: blue;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is an open shadow root</div>
    `;

    document.getElementById('open-shadow-btn').addEventListener('click', () => {
      openShadowRoot.querySelector('.shadow-content').textContent = 'Open Shadow Root content updated!';
    });
  </script>
</body>

En este ejemplo, se proporciona un botón para cambiar el contenido del shadow DOM. Como el shadow root está abierto, podemos acceder a su contenido y manipularlo desde el documento principal.

Shadow Root cerrado

Un shadow root cerrado restringe el acceso desde scripts externos, proporcionando un mejor encapsulamiento. En el siguiente ejemplo, intentamos manipular el contenido de texto dentro del shadow root después de que se crea, pero no es posible porque es closed.


html
<body>
  <div id="closed-shadow-host"></div>
  <button id="closed-shadow-btn">Try to Change Shadow Content</button>

  <script>
    const closedShadowHost = document.getElementById('closed-shadow-host');
    const closedShadowRoot = closedShadowHost.attachShadow({ mode: 'closed' });

    closedShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: red;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is a closed shadow root</div>
    `;

    // closedShadowHost.shadowRoot is null for closed roots, so this throws a TypeError
    document.getElementById('closed-shadow-btn').addEventListener('click', () => {
      try {
        closedShadowHost.shadowRoot.querySelector('.shadow-content').textContent = 'Attempted to update closed shadow root!';
      } catch (e) {
        alert('Cannot access shadow root content from outside!');
      }
    });
  </script>
</body>

En este ejemplo, el intento de acceder y manipular el contenido del shadow DOM falla porque el shadow root está cerrado. Ten en cuenta que, para los shadow roots cerrados, la propiedad shadowRoot en el elemento host es null, lo que hace que la llamada null.querySelector lance un TypeError. Esto demuestra cómo los shadow roots cerrados proporcionan un mejor encapsulamiento al restringir el acceso. Nota: los shadow roots cerrados son compatibles con todos los navegadores modernos, pero ocultan intencionadamente el root de JavaScript externo para reforzar el encapsulamiento. Úsalos cuando quieras evitar el acceso externo accidental.

Estilizado dentro de Shadow DOM

WARNING

Al implementar JavaScript Shadow DOM, asegúrate de un encapsulamiento adecuado para evitar conflictos de estilo o de scripting no deseados.

Los estilos definidos dentro de un shadow root no afectan a los elementos fuera de él, y viceversa. Este encapsulamiento es beneficioso para crear componentes reutilizables.


html
<head>
  <style>
    .styled-box {
      color: red;
      background-color: yellow;
      padding: 10px;
      border: 1px solid green;
    }
  </style>
</head>
<body>
  <div class="styled-box">This is styled by the main document</div>
  <div id="styled-host"></div>

  <script>
    const styledHost = document.getElementById('styled-host');
    const shadowRoot = styledHost.attachShadow({ mode: 'open' });

    shadowRoot.innerHTML = `
      <style>
        .styled-box {
          color: white;
          background-color: black;
          padding: 10px;
          border-radius: 5px;
        }
      </style>
      <div class="styled-box">Styled by Shadow DOM</div>
    `;
  </script>
</body>

En este ejemplo, hay dos elementos con el nombre de clase styled-box. El primer elemento recibe estilos del CSS del documento principal, mientras que el segundo elemento recibe estilos del CSS del Shadow DOM. Los estilos definidos en el Shadow DOM no afectan a los elementos del documento principal, y los estilos definidos en el documento principal no afectan a los elementos del Shadow DOM. Esto demuestra cómo Shadow DOM encapsula los estilos, garantizando que no haya conflictos entre los estilos del componente y los estilos globales.

Para dar estilo a elementos dentro del Shadow DOM desde fuera, usa Shadow Parts de CSS (::part()) y pseudo-elementos con slot (::slotted()). Estos permiten que CSS externo apunte a partes internas específicas o a contenido proyectado sin romper el encapsulamiento.

Slotting: contenido del Light DOM en Shadow DOM

Los slots permiten a los desarrolladores pasar contenido del light DOM (DOM normal) al shadow DOM, haciendo que el shadow DOM sea más flexible y reutilizable.


html
<div id="slot-host">
  <span slot="title">Shadow DOM Slot Example</span>
</div>

<script>
  const slotHost = document.getElementById('slot-host');
  const shadowRoot = slotHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <style>
      .container {
        border: 1px solid #ccc;
        padding: 10px;
      }
    </style>
    <div class="container">
      <h1><slot name="title"></slot></h1>
      <p>This is a Shadow DOM component with a slot for the title.</p>
    </div>
  `;
</script>

En este ejemplo, se usa el elemento <slot> para pasar contenido desde el light DOM al shadow DOM. El atributo slot en el elemento span coincide con el atributo name del elemento slot en el shadow DOM, lo que permite que el contenido del span se proyecte en el shadow DOM.

Interacción de JavaScript con Shadow DOM

Interactuar con el Shadow DOM mediante JavaScript requiere comprender los límites del encapsulamiento. La manipulación directa dentro del shadow root es sencilla, pero la interacción externa requiere un manejo cuidadoso.

Acceso a elementos del Shadow DOM

Para acceder a elementos dentro de un shadow DOM, usa la propiedad shadowRoot.


html
<div id="interactive-host"></div>

<script>
  const interactiveHost = document.getElementById('interactive-host');
  const shadowRoot = interactiveHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <button id="shadow-btn">Click me</button>
  `;

  const shadowButton = shadowRoot.querySelector('#shadow-btn');
  shadowButton.addEventListener('click', () => {
    alert('Button inside Shadow DOM clicked!');
  });
</script>

En este ejemplo, accedemos al botón dentro del shadow DOM usando querySelector sobre el shadow root. Como el shadow root está abierto, podemos adjuntar escuchadores de eventos y manipular elementos directamente desde el documento principal.

Ejemplos prácticos de Shadow DOM

Creación de un Web Component reutilizable

Crear un web component reutilizable usando Shadow DOM implica definir un elemento personalizado y adjuntarle un shadow root.


html
<body>
  <custom-card title="Hello World"></custom-card>

  <script>
    class CustomCard extends HTMLElement {
      constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
          <style>
            .card {
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 5px;
              box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            }
            .card-title {
              font-size: 1.2em;
              margin-bottom: 5px;
            }
          </style>
          <div class="card">
            <div class="card-title">${this.getAttribute('title')}</div>
            <div class="card-content"><slot></slot></div>
          </div>
        `;
      }
    }

    customElements.define('custom-card', CustomCard);
  </script>
</body>

En este ejemplo, se crea un elemento personalizado &lt;custom-card&gt; con un shadow DOM. El Shadow DOM encapsula los estilos y la estructura del componente, haciéndolo reutilizable sin preocuparte por conflictos de estilos con el documento principal.

Integración con frameworks

Shadow DOM puede usarse sin problemas con frameworks modernos de JavaScript como React, Angular y Vue.

Ejemplo en React

En React, puedes adjuntar un shadow DOM a un elemento contenedor de la siguiente manera:


html
<body>
  <div id="root"></div>
  <!-- React and ReactDOM CDN links -->
  <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script type="text/babel">
    const { useRef, useLayoutEffect } = React;

    const CustomCard = ({ title, content }) => {
      const cardRef = useRef(null);

      useLayoutEffect(() => {
        if (cardRef.current) {
          const shadowRoot = cardRef.current.attachShadow({ mode: 'open' });

          shadowRoot.innerHTML = `
            <style>
              .card {
                padding: 10px;
                border: 1px solid #ddd;
                border-radius: 5px;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
              }
              .card-title {
                font-size: 1.2em;
                margin-bottom: 5px;
              }
            </style>
            <div class="card">
              <div class="card-title">${title}</div>
              <div class="card-content">${content}</div>
            </div>
          `;
        }
      }, [title, content]);

      return <div ref={cardRef}></div>;
    };

    const App = () => (
      <CustomCard title="Hello World" content="This is content inside the shadow DOM.">
      </CustomCard>
    );

    const rootElement = document.getElementById('root');
    const root = ReactDOM.createRoot(rootElement);
    root.render(<App />);
  </script>
</body>

En este ejemplo, se crea un componente React CustomCard que adjunta un shadow DOM a un div normal. El Shadow DOM garantiza que los estilos y la estructura del componente estén encapsulados, proporcionando una integración fluida con React.

Conclusión

Dominar Shadow DOM es esencial para el desarrollo web moderno, ya que proporciona un encapsulamiento y una reutilización potentes. Al comprender e implementar los conceptos y ejemplos proporcionados, puedes crear componentes robustos y aislados que mejoren la mantenibilidad y la escalabilidad de tus aplicaciones web.

Esta guía completa debería servir como una base sólida para explorar y utilizar Shadow DOM en tus proyectos. Tanto si estás creando widgets sencillos como aplicaciones complejas, Shadow DOM ofrece el encapsulamiento y la flexibilidad necesarios para garantizar que tus componentes permanezcan aislados y sean manejables.

Práctica

¿Qué método se usa para crear un shadow root en JavaScript?

¿Te resulta útil?

Vista previa dual-run — compárala con las rutas Symfony en producción.