Shadow DOM 201

CSS y estilos

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

Introducción

Enfrentémoslo. El lenguaje de marcado sin estilo no tiene nada de atractivo. Por suerte para nosotros, la gente brillante que creó Web Components lo previó y no nos dejó esperando. El módulo de alcance de CSS define muchas opciones para aplicar diseño al contenido en un shadow tree.

Encapsulamiento de estilo

Una de las funciones principales de Shadow DOM es el límite de la sombra. Tiene muchas propiedades atractivas, pero una de las mejores es que proporciona un encapsulamiento de estilo de forma gratuita. Indicado 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 que está en ShadowRoot. Nuevamente, los estilos con alcance de forma predeterminada.
  • Las demás reglas de estilo definidas en esta página que se orientan a h3 no aparecen en mi contenido. Esto se debe a que los selectores no cruzan el límite de la sombra.

¿La moraleja de la historia? Tenemos un encapsulamiento de estilo del mundo exterior. Gracias, Shadow DOM.

Cómo aplicar diseño al elemento host

:host te permite seleccionar y diseñar el elemento que aloja un shadow tree:

<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 problema es que las reglas de la página superior tienen mayor especificidad que las reglas :host definidas en el elemento, pero menor especificidad que un atributo style definido en el elemento de host. Esto permite que los usuarios anulen tu estilo desde el exterior. Además, :host 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 segmentar el elemento host si coincide con un <selector>.

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

:host(.different) {
    ...
}

Cómo reaccionar a los estados de los usuarios

Un caso de uso común para :host es cuando creas un elemento personalizado y quieres reaccionar a los diferentes estados de los usuarios (:hover, :focus, :active, etcétera).

<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 temas a un elemento

La seudoclase :host-context(<selector>) coincide con el elemento de host si esta o cualquiera de sus principales coinciden con <selector>.

Un uso común de :host-context() es aplicar temas a un elemento según lo que lo rodea. 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 sea descendiente de un elemento con la clase .different:

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

De esta manera, puedes encapsular reglas de estilo en el Shadow DOM de un elemento que le den un estilo único, según su contexto.

Admitir varios tipos de host desde una shadow root

:host también sirve si creas una biblioteca de temas y deseas admitir estilos para 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>. */
}

Cómo aplicar diseño a componentes internos de Shadow DOM desde el exterior

El seudoelemento ::shadow y el combinador /deep/ son como tener una espada Vorpal de autoridad CSS. Permiten perforar el límite de Shadow DOM para diseñar elementos dentro de árboles de sombra.

El seudoelemento ::shadow

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

Por ejemplo, si un elemento aloja una shadow root, puedes escribir #host::shadow span {} para aplicar diseño a todos los intervalos dentro de su shadow tree.

<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 shadow tree que contiene encabezados h2. Para definir el estilo de esos encabezados desde la página principal, puedes escribir lo siguiente:

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

La combinación /deep/

El combinador /deep/ es similar a ::shadow, pero más potente. Ignora por completo todos los límites de sombras y cruza hacia cualquier cantidad de árboles de sombras. En pocas palabras, /deep/ te permite profundizar en las entrañas de un elemento 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 montón de elementos personalizados (cada uno con su propio shadow tree) o crear un elemento que se hereda de otro usando <shadow>.

Ejemplo (elementos personalizados): Selecciona todos los elementos <x-panel> que son 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 shadow tree:

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

Trabaja con querySelector()

Al igual que .shadowRoot abre árboles de sombra para el recorrido del DOM, las combinadoras abren árboles de sombras para el recorrido del selector. En lugar de escribir una cadena de locura anidada, 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 de HTML nativos son un desafío de estilo. Muchas personas se dan por vencidos y se rinden. Sin embargo, con ::shadow y /deep/, se puede aplicar estilo a cualquier elemento de la plataforma web que use Shadow DOM. Algunos ejemplos útiles son los tipos <input> y <video>:

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

Cómo crear hooks de estilo

La personalización es buena. En algunos casos, es posible que quieras hacer agujeros en el escudo de estilo de Shadow y crear ganchos para que otros puedan darle estilo.

Usa ::shadow y /deep/

Hay mucha potencia detrás de /deep/. Les brinda a los autores de componentes una forma de designar elementos individuales como con estilo o varios elementos como temáticos.

Ejemplo: Aplica diseño a todos los elementos que tienen la clase .library-theme e ignora todos los árboles de sombra:

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

Uso de pseudoelementos personalizados

Tanto WebKit como Firefox definen pseudoelementos para diseñar partes internas de elementos nativos del navegador. Un buen ejemplo es input[type=range]. Puedes definir el estilo de la barra deslizante <span style="color:blue">blue</span> si segmentas tus anuncios para ::-webkit-slider-thumb:

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

Así como los navegadores proporcionan hooks de estilo en algunos componentes internos, los autores del contenido de Shadow DOM pueden designar ciertos elementos para que puedan diseñarlos usuarios externos. Esto se hace a través de pseudoelementos personalizados.

Puedes designar un elemento como un seudoelemento personalizado mediante el atributo pseudo. Su valor, o nombre, debe tener el prefijo "x-". Si lo haces, se creará una asociación con ese elemento en el árbol de sombras y les darás a los extraños un carril designado para cruzar el límite de la sombra.

A continuación, te mostramos un ejemplo de cómo crear un widget de control deslizante personalizado y permitir que alguien aplique un estilo de color 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>

Uso de variables de CSS

Una forma eficaz de crear hooks de tema será a través de las variables de CSS. Básicamente, crear "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 estilo 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 integrador del elemento define esos valores a su gusto. Tal vez para coincidir con el tema supergenial de 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 se heredan las variables de CSS, todo es de color dura y funciona a la perfección. La imagen completa se ve así:

<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>

Restableciendo estilos

Los estilos heredados, como las fuentes, los colores y las alturas de línea, siguen afectando a los elementos del Shadow DOM. Sin embargo, para lograr la máxima flexibilidad, Shadow DOM nos brinda la propiedad resetStyleInheritance para controlar lo que sucede en el límite de shadow. Considéralo una manera de comenzar desde cero cuando creas un componente nuevo.

resetStyleInheritance

  • false: Predeterminado. Las propiedades heredables de CSS 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 se ve afectado el shadow tree cuando se cambia 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 Herramientas para desarrolladores

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

Si no sabes qué propiedades se heredan en CSS, consulta esta práctica lista o activa la casilla de verificación “Mostrar elementos heredados” en el panel Elemento.

Cómo aplicar diseño a 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 Light DOM y renderizarlos en ubicaciones predefinidas de tu Shadow DOM. No están lógicamente en el Shadow DOM; todavía son elementos secundarios del elemento host. Los puntos de inserción son solo una cosa de renderización.

Los nodos distribuidos retienen 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. De nuevo, los nodos distribuidos siguen, lógicamente, en el Lightdom 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 shadow tree.

Pseudoelemento de contenido

Los nodos distribuidos son secundarios del elemento del host. Entonces, ¿cómo podemos orientarlos desde dentro del Shadow DOM? La respuesta es el seudoelemento ::content de CSS. Es una forma de orientar los nodos de Light DOM que pasan por un punto de inserción. Por ejemplo:

::content > h3 diseñ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 estilos en puntos de inserción

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

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

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

Conclusión

Como autores de elementos personalizados, tenemos muchísimas opciones para controlar el aspecto de nuestro contenido. Shadow DOM constituye la base de este nuevo mundo.

Shadow DOM nos proporciona un encapsulamiento de estilo con alcance y un medio para dejar entrar la mayor cantidad (o mínima) del mundo exterior como elegimos. Cuando los autores definen pseudoelementos personalizados o incluyen marcadores de posición de variables de CSS, pueden proporcionar hooks de estilo convenientes de terceros para personalizar aún más su contenido. En conjunto, los autores web tienen control total sobre cómo se representa su contenido.