Shadow DOM 201

CSS e estilo

Neste artigo, discutimos mais sobre as coisas incríveis que você pode fazer com o Shadow DOM. Ele se baseia nos conceitos discutidos em Shadow DOM 101. Se você está procurando uma introdução, consulte este artigo.

Introdução

Vamos ser honestos. Não há nada de especial em uma marcação sem estilo. Por sorte, as pessoas incríveis por trás dos Web Components previram isso e não nos deixaram na mão. O Módulo de Escopo do CSS define muitas opções para definir o estilo do conteúdo em uma árvore de sombra.

Encapsulamento de estilo

Um dos principais recursos do shadow DOM é o limite da sombra. Ele tem muitas propriedades legais, mas uma das melhores é que ele oferece encapsulamento de estilo sem custo financeiro. Em outras palavras:

<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 e, portanto, tem estilo vermelho é o que está no ShadowRoot. Novamente, estilos com escopo por padrão.
  • As outras regras de estilo definidas nesta página que segmentam h3s não aparecem no meu conteúdo. Isso acontece porque os seletores não cruzam o limite da sombra.

Moral da história? Temos o encapsulamento de estilo do mundo exterior. Obrigado, Shadow DOM!

Como definir o estilo do elemento original

O :host permite selecionar e estilizar o elemento que hospeda uma árvore 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>

Uma pegadinha é que as regras na página mãe têm maior especificidade do que as regras :host definidas no elemento, mas menor especificidade do que um atributo style definido no elemento de host. Isso permite que os usuários substituam seu estilo de fora. :host também só funciona no contexto de uma ShadowRoot, então não é possível usá-lo fora do Shadow DOM.

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

Exemplo: corresponde apenas se o elemento tiver a classe .different (por exemplo, <x-foo class="different"></x-foo>):

:host(.different) {
    ...
}

Como reagir aos estados do usuário

Um caso de uso comum para :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>

Como aplicar um tema a um elemento

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

Um uso comum de :host-context() é para definir um tema para um elemento com base no que está ao redor dele. Por exemplo, muitas pessoas aplicam uma classe a <html> ou <body>:

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

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

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

Isso permite encapsular regras de estilo no Shadow DOM de um elemento que dá estilo exclusivo com base no contexto.

Suporte a vários tipos de host em uma raiz de sombra

Outro uso para :host é se você estiver criando uma biblioteca de temas e quiser oferecer suporte ao estilo de muitos tipos de elementos de 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 aplicar estilos em elementos internos do Shadow DOM de fora

O pseudoelemento ::shadow e o combinador /deep/ são como ter uma espada Vorpal de autoridade CSS. Elas permitem a perfuração da fronteira do shadow DOM para estilizar elementos em árvores shadow.

O pseudoelemento ::shadow

Se um elemento tiver pelo menos uma árvore de sombra, o pseudoelemento ::shadow vai corresponder à raiz da sombra. Ele permite que você escreva seletores que estilizam nós internos no DOM de sombra de um elemento.

Por exemplo, se um elemento hospedar uma raiz de sombra, você pode escrever #host::shadow span {} para estilizar todos os spans na árvore de sombra.

<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 filhos <x-panel> no shadow DOM. Cada painel hospeda a própria árvore de sombra com os títulos h2. Para estilizar esses títulos na página principal, é possível escrever:

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

O combinator /deep/

O combinador /deep/ é semelhante a ::shadow, mas mais poderoso. Ele ignora completamente todos os limites de sombra e atravessa qualquer número de árvores de sombra. Simplificando, /deep/ permite que você acesse as entranhas de um elemento e segmente qualquer nó.

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

Exemplo (elementos personalizados): selecione todos os elementos <x-panel> que são descendentes de <x-tabs>, em qualquer lugar da árvore:

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

Exemplo: estilize todos os elementos com a classe .library-theme em qualquer lugar em uma árvore de sombra:

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

Como trabalhar com querySelector()

Assim como .shadowRoot abre árvores de sombra para a travessia do DOM, os combinatores abrem árvores de sombra para a travessia do seletor. Em vez de escrever uma cadeia aninhada de loucura, 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');

Estilo de elementos nativos

Os controles HTML nativos são um desafio para estilizar. Muitas pessoas simplesmente desistem e fazem o próprio. No entanto, com ::shadow e /deep/, qualquer elemento na plataforma da Web que usa o Shadow DOM pode receber estilo. Bons exemplos são os tipos <input> e <video>:

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

Como criar ganchos de estilo

A personalização é boa. Em alguns casos, você pode querer fazer furos no escudo de estilo da Shadow e criar ganchos para que outras pessoas estilizem.

Como usar ::shadow e /deep/

O /deep/ tem muito poder. Ele oferece aos autores de componentes uma maneira de designar elementos individuais como estilizáveis ou uma série de elementos como temas.

Exemplo: estilize todos os elementos que têm a classe .library-theme, ignorando todas as árvores de sombra:

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

Como usar pseudoelementos personalizados

O WebKit e o Firefox definem pseudoelementos para estilizar partes internas de elementos nativos do navegador. Um bom exemplo é o input[type=range]. É possível estilizar o cursor 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;
}

Assim como os navegadores fornecem ganchos de estilo em alguns elementos internos, os autores de conteúdo do shadow DOM podem designar alguns elementos como estilizáveis por 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 ter o prefixo "x-". Isso cria uma associação com esse elemento na árvore de sombra e dá a terceiros uma faixa designada para cruzar o limite da sombra.

Confira um exemplo de como criar um widget de controle deslizante personalizado e permitir que alguém estilize o botão de controle deslizante em 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 temas é usando variáveis CSS. Basicamente, criar "marcadores de estilo" para que outros usuários preencham.

Imagine um autor de elemento personalizado que marca marcadores de posição variáveis no shadow DOM. Um para estilizar a fonte de um botão interno e outro para a cor dele:

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

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

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

Devido à forma como as variáveis CSS são herdadas, tudo é perfeito e isso funciona perfeitamente. A imagem completa fica assim:

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

Redefinir estilos

Estilos herdados, como fontes, cores e altura da linha, continuam afetando os elementos no Shadow DOM. No entanto, para ter a máxima flexibilidade, o Shadow DOM oferece a propriedade resetStyleInheritance para controlar o que acontece no limite da sombra. Pense nisso como uma forma de começar do zero ao criar um novo componente.

resetStyleInheritance

  • false: padrão. As propriedades CSS herdáveis continuam herdando.
  • true: redefine as propriedades herdáveis como initial na fronteira.

Confira abaixo uma demonstração que mostra como a árvore de sombra é afetada pela alteração 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 ele só afeta as propriedades CSS que são herdáveis. Ele diz: quando você está procurando uma propriedade para herdar, no limite entre a página e o ShadowRoot, não herde os valores do host, mas use o valor initial (de acordo com a especificação do CSS).

Se você não tiver certeza de quais propriedades são herdadas no CSS, confira esta lista útil ou ative a caixa de seleção "Mostrar herdado" no painel "Elemento".

Como aplicar estilos a 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. Elas não estão logicamente no DOM sombra, mas ainda são filhos do elemento host. Os pontos de inserção são apenas uma questão de renderização.

Os nós distribuídos retêm os estilos do documento principal. Ou seja, as regras de estilo da página principal continuam sendo aplicadas aos elementos, mesmo quando eles são renderizados em um ponto de inserção. Novamente, os nós distribuídos ainda estão logicamente no DOM leve 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. Como podemos segmentá-los dentro do shadow DOM? A resposta é o pseudoelemento ::content do CSS. É uma maneira de segmentar nós DOM leves que passam por um ponto de inserção. Exemplo:

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

Confira 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 nos pontos de inserção

Ao criar um 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 no JS ou use o atributo booleano reset-style-inheritance no próprio elemento.

  • Para um ponto 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 atingir o conteúdo da sombra. Esse local é conhecido como limite superior.

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

Conclusão

Como autores de elementos personalizados, temos muitas opções para controlar a aparência do nosso conteúdo. O shadow DOM é a base desse novo mundo.

O Shadow DOM oferece encapsulamento de estilo com escopo e uma maneira de permitir a entrada de tanto ou tão pouco do mundo externo quanto você escolher. Ao definir pseudoelementos personalizados ou incluir marcadores de posição de variável CSS, os autores podem fornecer ganchos de estilo convenientes a terceiros para personalizar ainda mais o conteúdo. Em resumo, os autores da Web têm controle total sobre como o conteúdo é representado.