Shadow DOM 201

CSS e estilo

Este artigo discute mais das coisas incríveis que você pode fazer com o Shadow DOM. Ele se baseia nos conceitos discutidos no Shadow DOM 101. Se você estiver procurando uma introdução, consulte este artigo.

Introdução

Sejamos honestos. Não há nada de sexy em uma marcação sem estilo. Para nossa sorte, as pessoas brilhantes por trás do Web Components previram isso e não nos deixaram sem resposta. O Módulo de escopo do CSS define muitas opções de estilo para o conteúdo em uma árvore paralela.

Encapsulamento de estilo

Um dos principais recursos do Shadow DOM é o limite do shadow. Ele tem muitas propriedades interessantes, mas uma das melhores é que fornece encapsulamento de estilo sem custos. Mencionada de outra forma:

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

Há duas observações interessantes sobre essa demonstração:

  • Há outros h3s nesta página, mas o único que corresponde ao seletor h3 (ou, portanto, estilizado) é o que está em ShadowRoot. Novamente, estilos com escopo por padrão.
  • Outras regras de estilo definidas nesta página que segmentam h3s não se misturam ao meu conteúdo. Isso ocorre porque os seletores não cruzam o limite do shadow (link em inglês).

Moral da história? Temos encapsulamento de estilo do mundo exterior. Agradecemos ao Shadow DOM!

Definir o estilo do elemento host

:host permite selecionar e estilizar o elemento que hospeda uma árvore paralela:

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

Um problema é que as regras na página mãe têm especificidade maior que as regras :host definidas no elemento, mas menor especificidade do que um atributo style definido no elemento host. Isso permite que os usuários modifiquem o estilo externamente. Como o :host só funciona no contexto de uma ShadowRoot, não é possível usá-la fora dele.

A forma funcional de :host(<selector>) permite segmentar o elemento host se ele corresponder a um <selector>.

Exemplo: faça correspondência somente se o próprio elemento tiver a classe .different (por exemplo, <x-foo class="different"></x-foo>):

:host(.different) {
    ...
}

Como reagir a estados do usuário

Um caso de uso comum de :host é quando você está criando um elemento personalizado e quer reagir a diferentes estados do usuário (: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>

Aplicação de temas em um elemento

A pseudoclasse :host-context(<selector>) corresponderá ao elemento host se ela ou qualquer um dos ancestrais corresponder a <selector>.

Um uso comum de :host-context() é aplicar temas a um elemento com base nos arredores. Por exemplo, muitas pessoas aplicam temas aplicando uma classe a <html> ou <body>:

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

É possível usar :host-context(.different) para definir o estilo de <x-foo> quando ele for descendente de um elemento com a classe .different:

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

Com isso, é possível encapsular regras de estilo no Shadow DOM de um elemento, que definem um estilo exclusivo com base no contexto dele.

Suporte a vários tipos de hosts de dentro de uma raiz paralela

O :host também pode ser usado se você estiver criando uma biblioteca de temas e quiser oferecer suporte ao estilo de vários tipos de elementos host no mesmo 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>. */
}

Como definir o estilo de componentes internos do Shadow DOM

O pseudoelemento ::shadow e o combinador /deep/ são como ter uma espada Vorpal de autoridade CSS. Eles permitem perfurar os limites do Shadow DOM para estilizar elementos dentro de árvores paralelas.

O pseudoelemento ::shadow

Se um elemento tiver pelo menos uma árvore paralela, o pseudoelemento ::shadow corresponderá à própria raiz paralela. Ela permite escrever seletores que estilizar nós internos no shadow DOM de um elemento.

Por exemplo, se um elemento hospedar uma raiz paralela, é possível programar #host::shadow span {} para definir o estilo de todos os períodos da árvore paralela.

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

Exemplo (elementos personalizados): <x-tabs> tem <x-panel> filhos no Shadow DOM. Cada painel hospeda a própria árvore paralela contendo títulos h2. Para estilizar esses títulos da página principal, é possível escrever:

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

O /deep/ combinator

O combinador /deep/ é semelhante ao ::shadow, mas é mais eficiente. Ele ignora completamente todos os limites de sombras e atravessa para qualquer número de árvores sombra. Simplificando, /deep/ permite analisar o interior de um elemento e segmentar qualquer nó.

O combinador /deep/ é particularmente útil no mundo dos elementos personalizados, em que é comum ter vários níveis do Shadow DOM. Os exemplos Prime são aninhar vários elementos personalizados (cada um hospedando sua própria árvore paralela) ou criar um elemento herdado de outro usando <shadow>.

Example (elementos personalizados): selecione todos os elementos <x-panel> descendentes de <x-tabs> em qualquer lugar da árvore:

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

Exemplo: defina o estilo de todos os elementos com a classe .library-theme em qualquer lugar de uma árvore paralela:

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

Como trabalhar com querySeletor()

Assim como .shadowRoot abre árvores paralelas para a travessia de DOM, os combinators abrem árvores paralelas para travessia de seletores. Em vez de escrever uma cadeia aninhada de loucuras, você pode escrever uma única instrução:

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

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

Como definir o estilo de elementos nativos

Controles HTML nativos são um desafio para definir o estilo. Muitas pessoas simplesmente desistem e fazem o próprio trabalho. No entanto, com ::shadow e /deep/, qualquer elemento na plataforma da Web que usa o Shadow DOM pode ser estilizado. Ótimos exemplos são os tipos de <input> e <video>:

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

Como criar ganchos de estilo

A personalização é boa. Em alguns casos, convém fazer furos no escudo de estilo da Sombra e criar ganchos para outras pessoas estilizarem.

Usando ::shadow e /deep/

Há muita energia por trás de /deep/. Ela oferece aos autores de componentes uma maneira de designar elementos individuais como estilizados ou vários elementos como temas.

Exemplo: defina o estilo de todos os elementos que tenham a classe .library-theme, ignorando todas as árvores sombra:

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

Como usar pseudoelementos personalizados

O WebKit e o Firefox definem pseudoelementos para definir o estilo de partes internas dos elementos nativos do navegador. Um bom exemplo é o input[type=range]. É possível definir o estilo da miniatura do controle deslizante <span style="color:blue">blue</span> segmentando ::-webkit-slider-thumb:

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

Da mesma forma que os navegadores fornecem ganchos de estilo para alguns componentes internos, os autores do conteúdo do Shadow DOM podem designar determinados elementos como estilosos de usuários externos. Isso é feito com pseudoelementos personalizados.

É possível designar um elemento como um pseudoelemento personalizado usando o atributo pseudo. O valor, ou nome, precisa ser prefixado com "x-". Fazer isso cria uma associação com esse elemento na árvore sombra e dá a pessoas externas uma faixa designada para cruzar o limite do sombra.

Confira um exemplo de como criar um widget de controle deslizante personalizado e permitir que alguém estilize o controle deslizante de azul:

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

Como usar variáveis CSS

Uma maneira eficiente de criar ganchos de aplicação de temas é usando as variáveis CSS. Basicamente, criar "marcadores de estilo" para que outros usuários preencham.

Imagine um autor de elemento personalizado que marque espaços reservados para variáveis no Shadow DOM. Um para estilizar a fonte de um botão interno e outro para sua cor:

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

Em seguida, o embedder do elemento define esses valores como quiser. Talvez para combinar com o tema superlegal Comic Sans da própria página:

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

Devido à maneira como as variáveis CSS herdam, tudo é em cor pêssego e isso funciona muito bem! O quadro completo é semelhante a este:

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

Redefinindo estilos

Estilos herdáveis, como fontes, cores e alturas de linha, continuam a afetar elementos no Shadow DOM. No entanto, para flexibilidade máxima, o Shadow DOM oferece a propriedade resetStyleInheritance para controlar o que acontece no limite do shadow. Pense nisso como uma maneira de começar do zero ao criar um novo componente.

resetStyleInheritance

  • false: padrão. As propriedades CSS herdáveis continuam sendo herdadas.
  • true: redefine as propriedades herdáveis para initial no limite.

Confira abaixo uma demonstração que mostra como a árvore paralela é afetada pela mudança 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>
Propriedades herdadas do DevTools

Entender .resetStyleInheritance é um pouco mais complicado, principalmente porque afeta apenas as propriedades CSS que são herdáveis. Ela diz: quando você está procurando uma propriedade para herdar, no limite entre a página e a ShadowRoot, não herde valores do host, mas use o valor initial (de acordo com a especificação CSS).

Se você não souber quais propriedades herdam no CSS, confira esta lista útil ou ative a caixa de seleção "Mostrar herdados" no painel "Elemento".

Como definir o estilo de nós distribuídos

Os nós distribuídos são elementos renderizados em um ponto de inserção (um elemento <content>). O elemento <content> permite selecionar nós do Light DOM e renderizá-los em locais predefinidos no Shadow DOM. Eles não estão logicamente no Shadow DOM e ainda são filhos do elemento host. Os pontos de inserção são apenas renderização.

Os nós distribuídos mantêm os estilos do documento principal. Ou seja, as regras de estilo da página principal continuam sendo aplicadas aos elementos, mesmo quando renderizados em um ponto de inserção. Novamente, os nós distribuídos ainda ficam logicamente no light dom e não se movem. Eles são renderizados em outro lugar. No entanto, quando os nós são distribuídos no Shadow DOM, eles podem assumir estilos adicionais definidos dentro da árvore shadow.

Pseudoelemento ::content

Os nós distribuídos são filhos do elemento host. Então, como podemos segmentá-los dentro do Shadow DOM? A resposta é o pseudoelemento CSS ::content. É uma forma de segmentar nós do Light DOM que passam por um ponto de inserção. Exemplo:

::content > h3 define o estilo de todas as tags h3 que passam por um ponto de inserção.

Vejamos um exemplo:

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

Redefinir estilos em pontos de inserção

Ao criar uma ShadowRoot, você tem a opção de redefinir os estilos herdados. Os pontos de inserção <content> e <shadow> também têm essa opção. Ao usar esses elementos, defina .resetStyleInheritance em JS ou use o atributo booleano reset-style-inheritance no próprio elemento.

  • Para pontos de inserção ShadowRoot ou <shadow>: reset-style-inheritance significa que as propriedades CSS herdáveis são definidas como initial no host, antes de chegarem ao conteúdo de sombra. Esse local é conhecido como o limite superior.

  • Para pontos de inserção <content>: reset-style-inheritance significa que as propriedades CSS herdáveis são definidas como initial antes que os filhos do host sejam distribuídos no ponto de inserção. Esse local é conhecido como o limite inferior.

Conclusão

Como autores de elementos personalizados, temos várias opções para controlar a aparência do nosso conteúdo. O Shadow DOM forma a base para esse novo mundo corajoso.

O Shadow DOM oferece encapsulamento de estilo com escopo e um meio de permitir a entrada do mundo externo quanto quisermos. Ao definir pseudoelementos personalizados ou incluir marcadores de variáveis CSS, os autores podem fornecer ganchos de estilo convenientes a terceiros para personalizar ainda mais o conteúdo. No geral, os autores da Web têm controle total de como o conteúdo é representado.