Shadow DOM 201

CSS y aplicación de estilos

En este artículo, se analizan más de las funciones increíbles que puedes hacer con Shadow DOM. Se basa en los conceptos que se analizaron en Shadow DOM 101. Si quieres obtener una introducción, consulta ese artículo.

Introducción

Enfrentémoslo. No hay nada atractivo en el marcado sin diseño. Por suerte, el equipo brillante detrás de Web Components lo previó y no nos dejó en el aire. El módulo de alcance de CSS define muchas opciones para aplicar diseño al contenido en un árbol de sombras.

Encapsulamiento de estilo

Una de las funciones principales del Shadow DOM es el límite de sombra. Tiene muchas propiedades interesantes, pero una de las mejores es que proporciona encapsulamiento de estilo de forma gratuita. Dicho de otra manera:

<div><h3>Light DOM</h3></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = `
  <style>
    h3 {
      color: red;
    }
  </style>
  <h3>Shadow DOM</h3>
`;
</script>

Hay dos observaciones interesantes sobre esta demostración:

  • Hay otros h3 en esta página, pero el único que coincide con el selector h3 y, por lo tanto, tiene el estilo en rojo, es el que está en ShadowRoot. Una vez más, los diseños centrados de forma predeterminada.
  • Las demás reglas de diseño definidas en esta página que se orientan a los h3 no se extienden a mi contenido. Esto se debe a que los selectores no cruzan el límite de la sombra.

¿Cuál es la moraleja de la historia? Tenemos encapsulamiento de estilo desde el mundo exterior. Gracias, Shadow DOM.

Aplica diseño al elemento host

:host te permite seleccionar y aplicar diseño al elemento que aloja un árbol de sombras:

<button class="red">My Button</button>
<script>
var button = document.querySelector('button');
var root = button.createShadowRoot();
root.innerHTML = `
  <style>
    :host {
      text-transform: uppercase;
    }
  </style>
  <content></content>
`;
</script>

Una trampa es que las reglas de la página superior tienen una especificidad más alta que las reglas :host definidas en el elemento, pero una especificidad más baja que un atributo style definido en el elemento host. Esto permite que los usuarios anulen tu diseño desde el exterior. :host también solo funciona en el contexto de un ShadowRoot, por lo que no puedes usarlo fuera de Shadow DOM.

La forma funcional de :host(<selector>) te permite segmentar el elemento host si coincide con un <selector>.

Ejemplo: Haz coincidir solo si el elemento tiene la clase .different (p.ej., <x-foo class="different"></x-foo>):

:host(.different) {
    ...
}

Cómo reaccionar a los estados del usuario

Un caso de uso común para :host es cuando creas un elemento personalizado y deseas reaccionar a diferentes estados del usuario (:hover, :focus, :active, etc.).

<style>
  :host {
    opacity: 0.4;
    transition: opacity 420ms ease-in-out;
  }
  :host(:hover) {
    opacity: 1;
  }
  :host(:active) {
    position: relative;
    top: 3px;
    left: 3px;
  }
</style>

Cómo aplicar un tema a un elemento

La pseudoclase :host-context(<selector>) coincide con el elemento host si este o cualquiera de sus ancestros coincide con <selector>.

Un uso común de :host-context() es aplicar un tema a un elemento según su entorno. Por ejemplo, muchas personas aplican temas aplicando una clase a <html> o <body>:

<body class="different">
  <x-foo></x-foo>
</body>

Puedes :host-context(.different) para aplicar diseño a <x-foo> cuando es un descendiente de un elemento con la clase .different:

:host-context(.different) {
  color: red;
}

Esto te permite encapsular reglas de estilo en el Shadow DOM de un elemento que le apliquen un estilo único, según su contexto.

Admite varios tipos de host desde una raíz de sombra

Otro uso de :host es si creas una biblioteca de temas y deseas admitir el diseño de muchos tipos de elementos de host desde el mismo Shadow DOM.

:host(x-foo) {
    /* Applies if the host is a <x-foo> element.*/
}

:host(x-foo:host) {
    /* Same as above. Applies if the host is a <x-foo> element. */
}

:host(div) {
    /* Applies if the host element is a <div>. */
}

Aplica diseño a los elementos internos de Shadow DOM desde el exterior

El pseudoelemento ::shadow y el combinador /deep/ son como tener una espada Vorpal de autoridad de CSS. Permiten atravesar el límite del Shadow DOM para aplicar diseño a los elementos dentro de los árboles de sombras.

El pseudoelemento ::shadow

Si un elemento tiene al menos un árbol de sombras, el pseudoelemento ::shadow coincide con la raíz de sombras. Te permite escribir selectores que aplican diseño a nodos internos del DOM de sombra de un elemento.

Por ejemplo, si un elemento aloja una raíz de sombra, puedes escribir #host::shadow span {} para aplicar diseño a todos los tramos dentro de su árbol de sombras.

<style>
  #host::shadow span {
    color: red;
  }
</style>

<div id="host">
  <span>Light DOM</span>
</div>

<script>
  var host = document.querySelector('div');
  var root = host.createShadowRoot();
  root.innerHTML = `
    <span>Shadow DOM</span>
    <content></content>
  `;
</script>

Ejemplo (elementos personalizados): <x-tabs> tiene elementos secundarios <x-panel> en su Shadow DOM. Cada panel aloja su propio árbol de sombras que contiene encabezados h2. Para aplicar diseño a esos encabezados de la página principal, se podría escribir lo siguiente:

x-tabs::shadow x-panel::shadow h2 {
    ...
}

El combinador /deep/

El combinador /deep/ es similar a ::shadow, pero más potente. Ignora por completo todos los límites de sombras y se cruza en cualquier cantidad de árboles de sombras. En pocas palabras, /deep/ te permite desglosar los elementos y segmentar cualquier nodo.

El combinador /deep/ es particularmente útil en el mundo de los elementos personalizados, donde es común tener varios niveles de Shadow DOM. Algunos ejemplos principales son anidar un grupo de elementos personalizados (cada uno aloja su propio árbol de sombras) o crear un elemento que hereda de otro con <shadow>.

Ejemplo (elementos personalizados): Selecciona todos los elementos <x-panel> que sean descendientes de <x-tabs>, en cualquier parte del árbol:

x-tabs /deep/ x-panel {
    ...
}

Ejemplo: Aplica diseño a todos los elementos con la clase .library-theme en cualquier lugar de un árbol de sombras:

body /deep/ .library-theme {
    ...
}

Cómo trabajar con querySelector()

Al igual que .shadowRoot abre los árboles de sombras para el recorrido del DOM, los combinadores abren los árboles de sombras para el recorrido del selector. En lugar de escribir una cadena anidada de locura, puedes escribir una sola sentencia:

// No fun.
document.querySelector('x-tabs').shadowRoot
        .querySelector('x-panel').shadowRoot
        .querySelector('#foo');

// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');

Aplica diseño a elementos nativos

Los controles HTML nativos son un desafío para aplicarles diseño. Muchas personas simplemente se rinden y crean su propio sitio. Sin embargo, con ::shadow y /deep/, se puede aplicar diseño a cualquier elemento de la plataforma web que use Shadow DOM. Algunos buenos ejemplos son los tipos <input> y <video>:

video /deep/ input[type="range"] {
  background: hotpink;
}

Crea hooks de estilo

La personalización es buena. En algunos casos, es posible que desees hacer agujeros en el escudo de diseño de tu sombra y crear hooks para que otros apliquen diseño.

Usa ::shadow y /deep/

/deep/ tiene mucha potencia. Les brinda a los autores de componentes una forma de designar elementos individuales como personalizables o una gran cantidad de elementos como personalizables con temas.

Ejemplo: Aplica diseño a todos los elementos que tengan la clase .library-theme, sin tener en cuenta todos los árboles de sombras:

body /deep/ .library-theme {
    ...
}

Usa pseudoelementos personalizados

Tanto WebKit como Firefox definen pseudoelementos para aplicar diseño a elementos internos de elementos nativos del navegador. Un buen ejemplo es input[type=range]. Puedes aplicar diseño al control deslizante <span style="color:blue">blue</span> si orientas ::-webkit-slider-thumb:

input[type=range].custom::-webkit-slider-thumb {
  -webkit-appearance: none;
  background-color: blue;
  width: 10px;
  height: 40px;
}

De manera similar a la forma en que los navegadores proporcionan hooks de diseño en algunos elementos internos, los autores del contenido de Shadow DOM pueden designar ciertos elementos como personalizables por terceros. Esto se hace a través de pseudelementos personalizados.

Puedes designar un elemento como un pseudoelemento personalizado con el atributo pseudo. Su valor o nombre debe tener el prefijo "x-". De esta manera, se crea una asociación con ese elemento en el árbol de sombras y se les brinda a los usuarios externos un carril designado para cruzar el límite de la sombra.

Este es un ejemplo de cómo crear un widget de control deslizante personalizado y permitir que alguien le asigne un estilo azul al control deslizante:

<style>
  #host::x-slider-thumb {
    background-color: blue;
  }
</style>
<div id="host"></div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <div>
      <div pseudo="x-slider-thumb"></div>' +
    </div>
  `;
</script>

Cómo usar variables de CSS

Una forma eficaz de crear hooks de temas es a través de las variables de CSS. En esencia, se crean "marcadores de posición de estilo" para que otros usuarios los completen.

Imagina un autor de elementos personalizados que marca marcadores de posición de variables en su Shadow DOM. Uno para aplicar diseño a la fuente de un botón interno y otro para su color:

button {
  color: var(--button-text-color, pink); /* default color will be pink */
  font-family: var(--button-font);
}

Luego, el incorporador del elemento define esos valores a su gusto. Quizás para que coincida con el tema Comic Sans de su propia página:

#host {
  --button-text-color: green;
  --button-font: "Comic Sans MS", "Comic Sans", cursive;
}

Debido a la forma en que heredan las variables CSS, todo está bien y funciona de forma excelente. La imagen completa se ve de la siguiente manera:

<style>
  #host {
    --button-text-color: green;
    --button-font: "Comic Sans MS", "Comic Sans", cursive;
  }
</style>
<div id="host">Host node</div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <style>
      button {
        color: var(--button-text-color, pink);
        font-family: var(--button-font);
      }
    </style>
    <content></content>
  `;
</script>

Restablecimiento de estilos

Los estilos heredables, como las fuentes, los colores y las alturas de línea, siguen afectando a los elementos del Shadow DOM. Sin embargo, para obtener la máxima flexibilidad, Shadow DOM nos brinda la propiedad resetStyleInheritance para controlar lo que sucede en el límite de la sombra. Piensa en ello como una forma de comenzar de cero cuando crees un componente nuevo.

resetStyleInheritance

  • false: Es la opción predeterminada. Las propiedades de CSS heredables siguen heredando.
  • true: Restablece las propiedades heredables en initial en el límite.

A continuación, se muestra una demostración que muestra cómo el árbol de sombras se ve afectado por el cambio de resetStyleInheritance:

<div>
  <h3>Light DOM</h3>
</div>

<script>
  var root = document.querySelector('div').createShadowRoot();
  root.resetStyleInheritance = <span id="code-resetStyleInheritance">false</span>;
  root.innerHTML = `
    <style>
      h3 {
        color: red;
      }
    </style>
    <h3>Shadow DOM</h3>
    <content select="h3"></content>
  `;
</script>

<div class="demoarea" style="width:225px;">
  <div id="style-ex-inheritance"><h3 class="border">Light DOM</div>
</div>
<div id="inherit-buttons">
  <button id="demo-resetStyleInheritance">resetStyleInheritance=false</button>
</div>

<script>
  var container = document.querySelector('#style-ex-inheritance');
  var root = container.createShadowRoot();
  //root.resetStyleInheritance = false;
  root.innerHTML = '<style>h3{ color: red; }</style><h3>Shadow DOM<content select="h3"></content>';

  document.querySelector('#demo-resetStyleInheritance').addEventListener('click', function(e) {
    root.resetStyleInheritance = !root.resetStyleInheritance;
    e.target.textContent = 'resetStyleInheritance=' + root.resetStyleInheritance;
    document.querySelector('#code-resetStyleInheritance').textContent = root.resetStyleInheritance;
  });
</script>
Propiedades heredadas de DevTools

Comprender .resetStyleInheritance es un poco más complicado, principalmente porque solo afecta a las propiedades de CSS que son heredables. Dice lo siguiente: Cuando buscas una propiedad para heredar, en el límite entre la página y el ShadowRoot, no heredes valores del host, sino que usa el valor initial (según las especificaciones de CSS).

Si no sabes qué propiedades heredan en CSS, consulta esta práctica lista o activa o desactiva la casilla de verificación "Mostrar heredado" en el panel Elemento.

Aplica diseño a los nodos distribuidos

Los nodos distribuidos son elementos que se renderizan en un punto de inserción (un elemento <content>). El elemento <content> te permite seleccionar nodos del DOM ligero y renderizarlos en ubicaciones predefinidas en tu DOM sombreado. No están lógicamente en el DOM sombreado, sino que siguen siendo elementos secundarios del elemento host. Los puntos de inserción son solo un elemento de renderización.

Los nodos distribuidos retienen los estilos del documento principal. Es decir, las reglas de estilo de la página principal se siguen aplicando a los elementos, incluso cuando se renderizan en un punto de inserción. Una vez más, los nodos distribuidos siguen lógicamente en el DOM ligero y no se mueven. Solo se renderizan en otro lugar. Sin embargo, cuando los nodos se distribuyen en el shadow DOM, pueden adoptar estilos adicionales definidos dentro del árbol de sombras.

Pseudoelemento ::content

Los nodos distribuidos son elementos secundarios del elemento host, por lo que, ¿cómo podemos segmentarlos desde dentro del Shadow DOM? La respuesta es el pseudoelemento ::content de CSS. Es una forma de segmentar nodos de DOM ligeros que pasan por un punto de inserción. Por ejemplo:

::content > h3 aplica diseño a cualquier etiqueta h3 que pase por un punto de inserción.

Veamos un ejemplo:

<div>
  <h3>Light DOM</h3>
  <section>
    <div>I'm not underlined</div>
    <p>I'm underlined in Shadow DOM!</p>
  </section>
</div>

<script>
var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = `
  <style>
    h3 { color: red; }
      content[select="h3"]::content > h3 {
      color: green;
    }
    ::content section p {
      text-decoration: underline;
    }
  </style>
  <h3>Shadow DOM</h3>
  <content select="h3"></content>
  <content select="section"></content>
`;
</script>

Restablece los estilos en los puntos de inserción

Cuando creas un ShadowRoot, tienes la opción de restablecer los estilos heredados. Los puntos de inserción <content> y <shadow> también tienen esta opción. Cuando uses estos elementos, establece .resetStyleInheritance en JS o usa el atributo booleano reset-style-inheritance en el elemento.

  • Para puntos de inserción de ShadowRoot o <shadow>, reset-style-inheritance significa que las propiedades CSS heredables se establecen en initial en el host, antes de que lleguen a tu contenido de sombra. Esta ubicación se conoce como límite superior.

  • Para los puntos de inserción <content>: reset-style-inheritance significa que las propiedades CSS heredables se establecen en initial antes de que los elementos secundarios del host se distribuyan en el punto de inserción. Esta ubicación se conoce como límite inferior.

Conclusión

Como autores de elementos personalizados, tenemos muchas opciones para controlar el aspecto de nuestro contenido. El shadow DOM forma la base de este nuevo mundo.

Shadow DOM nos brinda un encapsulamiento de estilo con alcance limitado y un medio para permitir la entrada de la cantidad que deseemos (o la menor cantidad posible) del mundo exterior. Cuando se definen pseudoelementos personalizados o se incluyen marcadores de posición de variables de CSS, los autores pueden proporcionar a terceros ganchos de diseño convenientes para personalizar aún más su contenido. En resumen, los autores web tienen el control total sobre cómo se representa su contenido.