Shadow DOM 201

CSS y aplicación de estilos

En este artículo, se analizan más sobre las asombrosas cosas 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 de Shadow DOM es el límite de sombra. Tiene muchas propiedades atractivas, pero una de las mejores es que proporciona un encapsulamiento de estilos 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 un estilo rojo, es el de ShadowRoot. Una vez más, los diseños centrados de forma predeterminada.
  • Las demás reglas de estilo definidas en esta página que se orientan a los h3 no se filtran en mi contenido. Eso se debe a que los selectores no cruzan el límite de la sombra.

¿Cuál es la moraleja de la historia? Tenemos el 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>

Un aspecto desfavorable es que las reglas de la página principal tienen más especificidad que las reglas :host definidas en el elemento, pero menos especificidad que un atributo style definido en el elemento host. Esto permite que los usuarios anulen tus estilos desde el exterior. :host también solo funciona en el contexto de un ShadowRoot, por lo que no puedes usarlo fuera de Shadow DOM.

El formato funcional de :host(<selector>) te permite orientar el elemento de host si coincide con una <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 seudoclase :host-context(<selector>) coincide con el elemento del host si este o cualquiera de sus principales coinciden con <selector>.

Un uso común de :host-context() es para aplicar temas a un elemento en función de 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 aplicarle diseño a <x-foo> cuando sea 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 misma shadow root

Otro uso de :host es si creas una biblioteca de temas y quieres admitir el diseño de muchos tipos de elementos de host dentro del 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 los límites de Shadow DOM para darle estilo a los elementos dentro de los árboles sombreados.

El pseudoelemento ::shadow

Si un elemento tiene al menos un árbol de sombras, el pseudoelemento ::shadow coincide con la raíz en las sombras. Te permite escribir selectores que apliquen 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 definir el estilo de esos encabezados desde la página principal, podrías 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 parte del shadow tree:

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');

Cómo aplicar estilo a elementos nativos

Los controles HTML nativos son un desafío para aplicarles diseño. Muchas personas se dan por vencidos y lanzan los suyos. 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 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 aptos para que los usuarios externos les apliquen diseño. Esto se hace a través de pseudelementos personalizados.

Puedes designar un elemento como un seudoelemento personalizado mediante 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 aplique 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 de 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>

Cómo restablecer 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: Predeterminado. Las propiedades de CSS heredables se siguen heredando.
  • true: Restablece las propiedades heredables a 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 lista práctica o activa la casilla de verificación “Mostrar heredada” en el panel Elementos.

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 shadow DOM. No están lógicamente en el shadow DOM, 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 seudoelemento CSS ::content. 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>

Cómo restablecer diseños en los puntos de inserción

Cuando creas un ShadowRoot, tienes la opción de restablecer los estilos heredados. <content> y <shadow> puntos de inserción 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 ShadowRoot o <shadow>: reset-style-inheritance significa que las propiedades de CSS heredables se configuran en initial en el host, antes de que se incluya tu contenido paralelo. 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 muchísimas opciones para controlar la apariencia de nuestro contenido. Shadow DOM constituye la base de este mundo nuevo y valiente.

Shadow DOM nos brinda un encapsulamiento de estilo con alcance y un medio para dejar entrar todo el mundo exterior (o tan poco) como queramos. Mediante la definición de seudoelementos personalizados o la inclusión de marcadores de posición de variables CSS, los autores pueden proporcionar hooks de estilo convenientes a terceros para personalizar aún más su contenido. En resumen, los autores web tienen el control total de cómo se representa su contenido.