CSS y diseño
En este artículo, se analizan más de las increíbles cosas que puedes hacer con el Shadow DOM. Se basa en los conceptos que se analizan en Shadow DOM 101. Si buscas una introducción, consulta ese artículo.
Introducción
Enfrentémoslo. No hay nada atractivo en el lenguaje de marcado sin diseño. Por suerte, las personas brillantes detrás de los componentes web previeron esto y no nos dejaron sin opciones. El módulo de alcance de CSS define muchas opciones para aplicar diseño al contenido en un árbol de sombra.
Encapsulamiento de estilo
Una de las funciones principales del Shadow DOM es el límite de sombra. Tiene muchas propiedades agradables, pero una de las mejores es que proporciona encapsulación de estilo de forma gratuita. En otras palabras:
<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 estilo rojo, es el que está en ShadowRoot. Nuevamente, los estilos tienen alcance de forma predeterminada.
- Otras reglas de estilo definidas en esta página que se orientan a h3 no se aplican a mi contenido. Esto se debe a que los selectores no cruzan el límite de sombra.
¿Cuál es la moraleja de la historia? Tenemos encapsulación de estilo del mundo exterior. ¡Gracias, Shadow DOM!
Cómo aplicar diseño al elemento host
La seudoclase :host te permite seleccionar y aplicar diseño al elemento que aloja un árbol de sombra:
<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 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 del Shadow DOM.
La forma funcional de :host(<selector>) te permite orientar 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) {
...
}
Reacciona a los estados del usuario
Un caso de uso común para :host es cuando creas un elemento personalizado y quieres 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 coinciden con <selector>.
Un uso común de :host-context() es aplicar un tema a un elemento en función de su entorno. Por ejemplo,
muchas personas aplican temas con una clase a <html> o <body>:
<body class="different">
<x-foo></x-foo>
</body>
Puedes usar :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 aplican un estilo único, según su contexto.
Admite varios tipos de host desde una raíz de sombra
Otro uso de :host es si estás creando una biblioteca de temas y quieres 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>. */
}
Cómo aplicar diseño a los elementos internos del 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 sombra.
El pseudoelemento ::shadow
Si un elemento tiene al menos un árbol de sombra, el pseudoelemento ::shadow coincide con la raíz de sombra.
Te permite escribir selectores que aplican diseño a los nodos internos del DOM de sombra de un elemento.
Por ejemplo, si un elemento aloja una raíz de shadow, puedes escribir #host::shadow span {} para aplicar diseño a todos los intervalos dentro de su árbol de shadow.
<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 <x-panel> elementos secundarios en su Shadow DOM. Cada panel aloja su propio árbol de sombra que contiene encabezados h2. Para aplicar diseño a esos encabezados desde 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 sombra y cruza cualquier cantidad de árboles de sombra. 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, en el que es común tener varios niveles de Shadow DOM. Los ejemplos principales son anidar un grupo de elementos personalizados (cada uno con su propio árbol de sombra) o crear un elemento que herede 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 sombra:
body /deep/ .library-theme {
...
}
Cómo trabajar con querySelector()
Al igual que .shadowRoot abre árboles de sombra para el recorrido del DOM, los combinadores abren árboles de sombra para el recorrido de selectores.
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 diseño a los elementos nativos
Los controles HTML nativos son un desafío para aplicarles diseño. Muchas personas simplemente se rinden
y crean su propia solución. Sin embargo, con ::shadow y /deep/, se puede aplicar diseño a cualquier elemento de la plataforma web que use Shadow DOM. Algunos ejemplos excelentes son los tipos <input> y <video>:
video /deep/ input[type="range"] {
background: hotpink;
}
Crea hooks de estilo
La personalización es buena. En ciertos casos, es posible que quieras abrir agujeros en el escudo de diseño de tu Shadow y crear hooks para que otros los diseñen.
Usa ::shadow y /deep/
/deep/ tiene mucho poder. Les brinda a los autores de componentes una forma de designar
elementos individuales como personalizables o un conjunto de elementos como adaptables a temas.
Ejemplo: Aplica diseño a todos los elementos que tienen la clase .library-theme, ignorando todos los árboles de sombra:
body /deep/ .library-theme {
...
}
Usa pseudoelementos personalizados
Tanto WebKit como Firefox definen seudoelementos para aplicar diseño a partes internas de los elementos nativos del navegador. Un buen ejemplo
es el input[type=range]. Puedes aplicar diseño al control deslizante <span style="color:blue">blue</span> con ::-webkit-slider-thumb:
input[type=range].custom::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: blue;
width: 10px;
height: 40px;
}
De manera similar a cómo los navegadores proporcionan hooks de diseño en algunos elementos internos, los autores de contenido de Shadow DOM pueden designar ciertos elementos como diseñables por usuarios externos. Esto se hace a través de pseudoelementos 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 sombra y se les da a los usuarios externos un
carril designado para cruzar el límite de sombra.
A continuación, se muestra 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 será 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 que el autor de un elemento personalizado 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 según sus preferencias. Tal vez para que coincida con el tema súper genial 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 funciona de maravillas. 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>
Restablece los 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 sombra.
Piensa en esto como una forma de empezar de cero cuando creas un componente nuevo.
resetStyleInheritance
false: Es el valor predeterminado. Las propiedades de CSS heredables se siguen heredando.true: Restablece las propiedades heredables ainitialen el límite.
A continuación, se muestra una demostración de cómo se ve afectado el árbol de sombra 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>
Comprender .resetStyleInheritance es un poco más difícil, principalmente porque
solo afecta a las propiedades de CSS que se pueden heredar. Dice lo siguiente: cuando buscas una propiedad para heredar, en el límite entre la página y ShadowRoot, no heredes valores del host, sino que usa el valor initial (según la especificación de CSS).
Si no tienes certeza de qué propiedades se heredan en CSS, consulta esta práctica lista o marca la casilla de verificación "Mostrar heredadas" en el panel Elemento.
Cómo aplicar 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 claro y renderizarlos en ubicaciones predefinidas en tu DOM de sombra. No están lógicamente en el DOM de sombra, sino que siguen siendo elementos secundarios del elemento host. Los puntos de inserción son solo un elemento de renderización.
Los nodos distribuidos conservan 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. Nuevamente, los nodos distribuidos siguen estando lógicamente en el DOM de luz 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 sombra.
Pseudoelemento ::content
Los nodos distribuidos son elementos secundarios del elemento host, por lo que, ¿cómo podemos orientarlos
desde el Shadow DOM? La respuesta es el pseudoelemento ::content de CSS.
Es una forma de segmentar los nodos del Light DOM 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 un ShadowRoot o puntos de inserción
<shadow>:reset-style-inheritancesignifica que las propiedades CSS heredables se establecen eninitialen 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-inheritancesignifica que las propiedades de CSS heredables se establecen eninitialantes de que se distribuyan los elementos secundarios del host en el punto de inserción. Esta ubicación se conoce como el límite inferior.
Conclusión
Como autores de elementos personalizados, tenemos muchas opciones para controlar el aspecto de nuestro contenido. El Shadow DOM constituye la base de este nuevo mundo.
El Shadow DOM nos brinda una encapsulación de estilo con alcance y un medio para permitir que entre tanta (o tan poca) información del mundo exterior como elijamos. Cuando definen pseudoelementos personalizados o incluyen marcadores de posición de variables de CSS, los autores pueden proporcionar a terceros hooks de diseño convenientes para personalizar aún más su contenido. En resumen, los autores web tienen el control total de cómo se representa su contenido.