CSS Scoping en Shadow DOM

Dar estilo al componente desde fuera del mismo


Hemos comentado que cuando tenemos un componente con Shadow DOM, este se encuentra aislado del exterior. No tenemos ninguna forma de cambiar los estilos del componente desde fuera. Esto no es del todo cierto, ya que hay algunas excepciones específicas que trataremos en este artículo.

Antes de comenzar, recordemos la siguiente estructura HTML de un componente con Shadow DOM:

<html>
  <body>
    <div class="container">
      <app-element>
        #shadow-root
          <div class="container">
            Contenido del componente
          </div>
      </app-element>
    </div>
  </body>
</html>

Observa que el Shadow DOM comienza desde el #shadow-root en adelante, por lo que todos los elementos que se encuentran fuera de este #shadow-root están en el DOM global del documento, y por lo tanto, no están aislados.

Desde el DOM global

Un componente no es más que un elemento HTML personalizado, es decir, un elemento HTML al fin y al cabo. Como el Shadow DOM existe sólo dentro de él (que es lo que está aislado y no podemos alterar), si que podemos cambiar de forma global los estilos del elemento contenedor del componente, es decir, la propia etiqueta del componente:

app-element {
  display: block;
  background: indigo;
  color: white;
  padding: 10px;
  margin: 5px;
}

Estos estilos si afectarán al componente, por lo que podemos utilizarlos para darle estilo desde fuera si lo necesitamos. Recuerda que es necesario colocar un display, ya que por defecto, un componente tiene valor display: inline y muchas propiedades no le afectan.

La pseudoclase :host

Sin embargo, desde el interior del Shadow DOM, debido al aislamiento, perdemos el acceso al exterior del componente. No obstante, tenemos a nuestra disposición la pseudoclase :host que no es más que eso: un sistema de acceder al elemento contenedor que contiene el Shadow DOM, como lo hicimos justo antes desde el ámbito global, pero desde dentro del componente:

<app-element></app-element>
class AppElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = /* html */`
      <style>
        :host {
          display: block;
          background: indigo;
          color: white;
          padding: 10px;
          margin: 5px;
        }
        span {
          font-weight: bold;
          vertical-align: super;
          font-size: small;
          color: gold;
        }
      </style>
      <div class="element">
        Contenido del componente <span>New!</span>
      </div>
    `;
  }
};

customElements.define("app-element", AppElement);

Mediante la pseudoclase :host, vemos que el componente busca el contenedor padre que contiene el Shadow DOM y le aplica los estilos definidos. Ten en cuenta que un custom element es un elemento inline, por lo que algunas propiedades no harán efecto si no le cambias su forma de representación.

La pseudoclase funcional :host()

En el caso de la pseudoclase funcional :host(css), podemos indicar un selector css entre paréntesis, de modo que solo seleccionará los custom elements que encajen con dicho selector. Esto es útil para aplicar sólo a componentes con ciertas clases o atributos específicos.

En nuestro ejemplo, le vamos a aplicar un fondo gris a todo aquel componente que tenga el atributo disabled, que en CSS se selecciona utilizando [disabled]:

class AppElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = /* html */`
      <style>
        :host {
          display: inline-block;
          padding: 10px;
          background: steelblue;
          border: 5px outset #6666;
        }

        :host([disabled]) {
          background: grey;
          cursor: not-allowed;
        }
        :host([disabled]) span {
          color: black;
        }

        span {
          font-weight: bold;
          vertical-align: super;
          font-size: small;
          color: red;
        }
      </style>
      <div class="element">
        AppElement <span>New!</span>
      </div>
    `;
  }
};

customElements.define("app-element", AppElement);
<app-element></app-element>
<app-element disabled></app-element>

Observa que con :host([disabled]) aplicas los estilos a los componentes del tipo <app-element> que tengan el atributo disabled, pero se podría utilizar con :host(.primary) para los componentes que tengan la clase .primary, etc. Por otro lado, también podemos utilizar :host([disabled]) span para cambiar los estilos a los elementos <span> que estén en el interior de un componente que tenga atributo disabled.

La pseudoclase :host-context()

Si todo esto te ha parecido interesante, nos queda por ver una pseudoclase aún más potente y versátil, que da muchas posibilidades: la pseudoclase funcional :host-context(css). Con ella, lo que hacemos es seleccionar los custom elements que se encuentren en un contexto que encaje con el selector especificado entre paréntesis.

Dicho de otra forma, podemos seleccionar los componentes que estén dentro de los selectores indicados. En nuestro ejemplo, aplicamos estilos al componente que se encuentre dentro de un elemento con clase .dark:

class AppElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = /* html */`
      <style>
        :host {
          display: block;
          background: indigo;
          color: white;
          padding: 10px;
          margin: 5px;
        }
        :host-context(.dark) {
          background: black;
        }
        span {
          font-weight: bold;
          vertical-align: super;
          font-size: small;
          color: gold;
        }
      </style>
      <div class="element">
        AppElement <span>New!</span>
      </div>
    `;
  }
};

customElements.define("app-element", AppElement);
<app-element></app-element>
<div class="dark">
  <app-element></app-element>
</div>

Esta pseudoclase nos proporciona posibilidades muy interesantes, sin embargo, la pega es que aún no está soportada en algunos navegadores, por lo que hay que usarla con cuidado.

La pseudoclase :defined

Por último, y no por ello menos interesante, tenemos la pseudoclase :defined, mediante la cuál podemos aplicar estilos dependiendo de si el elemento ha sido o no definido en el navegador. Por ejemplo, en el proceso inicial de carga de la página pueden existir momentos en los que el navegador aún no conoce el custom element que va a utilizar y no se encuentra cargado en el registro de custom elements del navegador.

app-element:not(:defined) {
  display: block;
  height: 46px;
  background: linear-gradient(grey, lightgrey);
}

app-element:defined {
  display: block;
  border: 2px solid red;
}

En el primer bloque estamos indicando que cuando el custom element <app-element> no esté definido, muestre una caja con un gradiente gris, pero desde que ya esté definido (segundo bloque), aparezca con un borde rojo. Obviamente, estos últimos estilos podrían ir dentro del componente.

Más información en el ciclo de vida de un componente.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev