W3docs

JavaScript Shadow DOM

O Shadow DOM permite anexar uma árvore DOM encapsulada a um elemento, isolando marcação, estilos e scripts do restante da página. Aprenda a criar shadow roots, escopar estilos, usar slots e criar componentes reutilizáveis.

O Shadow DOM é um bloco fundamental dos Web Components, permitindo que você anexe uma árvore DOM encapsulada e um escopo de estilo isolado a um elemento. Este guia aborda o que é o Shadow DOM, por que ele é importante, como criar shadow roots abertas e fechadas, escopar estilos, projetar conteúdo com slots e conectar tudo dentro de um elemento personalizado reutilizável.

O que é Shadow DOM?

O Shadow DOM permite anexar uma subárvore DOM separada e oculta a um elemento. A marcação e os estilos dentro dessa subárvore são encapsulados: eles não vazam para fora, e os estilos globais não vazam para dentro. Isso resolve um dos problemas mais antigos no desenvolvimento front-end — CSS global e IDs colidindo entre componentes.

Alguns termos merecem ser definidos de antemão:

  • Shadow host — o elemento regular ao qual a árvore shadow é anexada.
  • Shadow root — o nó raiz da árvore oculta, retornado por attachShadow().
  • Shadow tree — o DOM dentro do shadow root.
  • Light DOM — os filhos comuns do elemento, escritos em marcação normal; eles podem ser projetados na shadow tree por meio de slots.

O próprio navegador usa Shadow DOM internamente: os controles de um elemento <video> ou <input type="range"> vivem em uma shadow tree que você não pode acessar, e é exatamente por isso que seus internos nunca colidem com o seu CSS.

No exemplo abaixo, dois elementos compartilham a classe shadow-box, mas cada um mantém seu próprio estilo porque um vive no documento principal e o outro dentro de um shadow root.

<head>
  <style>
    .shadow-box {
      padding: 10px;
      border: 1px solid #000;
      background-color: lightcoral;
      color: white;
    }
  </style>
</head>
<body>
  <div class="shadow-box">This is styled by the main document</div>
  <div id="host"></div>
  <script>
    // Create a shadow root
    const hostElement = document.getElementById('host');
    const shadowRoot = hostElement.attachShadow({ mode: 'open' });

    // Attach shadow DOM content
    shadowRoot.innerHTML = `
      <style>
        .shadow-box {
          padding: 10px;
          border: 1px solid #000;
          background-color: lightblue;
          color: black;
        }
      </style>
      <div class="shadow-box">Hello, Shadow DOM!</div>
    `;
  </script>
</body>

Neste exemplo, existem dois elementos com o mesmo nome de classe shadow-box. O primeiro elemento é estilizado pelo CSS do documento principal, enquanto o segundo é estilizado pelo CSS do Shadow DOM. Como você pode ver, os estilos definidos no Shadow DOM não afetam os elementos no documento principal e vice-versa. Isso demonstra o encapsulamento fornecido pelo Shadow DOM, permitindo criar componentes isolados e reutilizáveis sem se preocupar com conflitos de estilo.

Criando um Shadow Root

Para criar um shadow root, use o método attachShadow em um elemento. O shadow root pode ser open (aberto) ou closed (fechado). Um shadow root open é acessível pelo JavaScript externo à shadow tree, enquanto um shadow root closed não é.

Shadow Root Aberto

Um shadow root aberto permite acesso e manipulação por JavaScript externo. No exemplo abaixo, manipulamos o conteúdo de texto dentro do shadow root após sua criação.

<body>
  <div id="open-shadow-host"></div>
  <button id="open-shadow-btn">Change Shadow Content</button>

  <script>
    const openShadowHost = document.getElementById('open-shadow-host');
    const openShadowRoot = openShadowHost.attachShadow({ mode: 'open' });

    openShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: blue;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is an open shadow root</div>
    `;

    document.getElementById('open-shadow-btn').addEventListener('click', () => {
      openShadowRoot.querySelector('.shadow-content').textContent = 'Open Shadow Root content updated!';
    });
  </script>
</body>

Neste exemplo, um botão é fornecido para alterar o conteúdo do Shadow DOM. Como o shadow root é aberto, podemos acessar e manipular seu conteúdo a partir do documento principal.

Shadow Root Fechado

Um shadow root fechado restringe o acesso de scripts externos, proporcionando melhor encapsulamento. No exemplo abaixo, tentamos manipular o conteúdo de texto dentro do shadow root após sua criação, mas não é possível pois ele está closed.

<body>
  <div id="closed-shadow-host"></div>
  <button id="closed-shadow-btn">Try to Change Shadow Content</button>

  <script>
    const closedShadowHost = document.getElementById('closed-shadow-host');
    const closedShadowRoot = closedShadowHost.attachShadow({ mode: 'closed' });

    closedShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: red;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is a closed shadow root</div>
    `;

    // closedShadowHost.shadowRoot is null for closed roots, so this throws a TypeError
    document.getElementById('closed-shadow-btn').addEventListener('click', () => {
      try {
        closedShadowHost.shadowRoot.querySelector('.shadow-content').textContent = 'Attempted to update closed shadow root!';
      } catch (e) {
        alert('Cannot access shadow root content from outside!');
      }
    });
  </script>
</body>

Aqui a tentativa falha porque o shadow root está fechado: closedShadowHost.shadowRoot retorna null, então null.querySelector(...) lança um TypeError e o bloco catch é executado. A referência retornada por attachShadow({ mode: 'closed' }) é a única maneira de alcançar essa árvore, portanto mantenha-a privada dentro do seu componente.

Um equívoco comum é que closed torna um componente verdadeiramente seguro — isso não é verdade. Ele apenas desencoraja o acesso externo casual; código que mantém a referência original ao root (ou que substitui attachShadow) ainda pode acessar a árvore. Use open a menos que você tenha uma razão concreta para ocultar os internos, pois open facilita muito a depuração e os testes.

Aspectomode: 'open'mode: 'closed'
host.shadowRootRetorna o shadow rootRetorna null
Acesso externoPermitido via host.shadowRootApenas pela referência salva
Uso típicoA maioria dos componentes, depuração fácilOcultar internos de scripts da página
Inspeção no DevToolsTotalmente visívelVisível, mas mais difícil de acessar via script

Estilização dentro do Shadow DOM

Aviso

Ao implementar o JavaScript Shadow DOM, garanta o encapsulamento adequado para evitar conflitos não intencionais de estilo ou scripts.

Os estilos definidos dentro de um shadow root não afetam elementos fora dele, e vice-versa. Esse encapsulamento é benéfico para criar componentes reutilizáveis.

<head>
  <style>
    .styled-box {
      color: red;
      background-color: yellow;
      padding: 10px;
      border: 1px solid green;
    }
  </style>
</head>
<body>
  <div class="styled-box">This is styled by the main document</div>
  <div id="styled-host"></div>

  <script>
    const styledHost = document.getElementById('styled-host');
    const shadowRoot = styledHost.attachShadow({ mode: 'open' });

    shadowRoot.innerHTML = `
      <style>
        .styled-box {
          color: white;
          background-color: black;
          padding: 10px;
          border-radius: 5px;
        }
      </style>
      <div class="styled-box">Styled by Shadow DOM</div>
    `;
  </script>
</body>

Neste exemplo, existem dois elementos com o nome de classe styled-box. O primeiro elemento é estilizado pelo CSS do documento principal, enquanto o segundo é estilizado pelo CSS do Shadow DOM. Os estilos definidos no Shadow DOM não afetam os elementos no documento principal, e os estilos definidos no documento principal não afetam os elementos no Shadow DOM. Isso demonstra como o Shadow DOM encapsula os estilos, garantindo que não haja conflitos entre os estilos do componente e os estilos globais.

Seletores Especiais para Shadow DOM

O encapsulamento não significa isolamento total. Três seletores oferecem ganchos controlados através da fronteira:

  • :host — usado dentro da shadow tree para estilizar o próprio elemento host. :host(.active) corresponde apenas quando o host carrega essa classe.
  • ::slotted(selector) — usado dentro da shadow tree para estilizar nós do Light DOM projetados em um slot. Ele pode atingir apenas os elementos slotted de nível superior, não seus descendentes.
  • ::part(name) — usado no documento externo para estilizar um elemento interno que o componente expõe explicitamente com um atributo part="name". Esta é a forma sancionada de permitir que consumidores tematizem um componente sem acessar seus internos.
<body>
  <div id="theme-host">
    <span>Projected from the light DOM</span>
  </div>

  <style>
    /* Outer page can only reach parts the component exposes */
    #theme-host::part(label) {
      text-decoration: underline;
    }
  </style>

  <script>
    const host = document.getElementById('theme-host');
    const root = host.attachShadow({ mode: 'open' });

    root.innerHTML = `
      <style>
        :host { display: block; padding: 10px; border: 2px solid teal; }
        .label { font-weight: bold; color: teal; }
        ::slotted(span) { color: crimson; }
      </style>
      <div class="label" part="label">Styled with :host and ::part</div>
      <slot></slot>
    `;
  </script>
</body>

A regra :host contorna todo o componente, .label é interno e privado, ::slotted(span) colore o texto do Light DOM projetado, e ::part(label) permite que a página externa sublinhe o rótulo para o qual recebeu permissão de tematizar. Tudo que não for exposto como part permanece intocável de fora.

Slots: Conteúdo Light DOM no Shadow DOM

Os slots permitem que desenvolvedores passem conteúdo do light DOM (DOM regular) para um Shadow DOM, tornando-o mais flexível e reutilizável.

<div id="slot-host">
  <span slot="title">Shadow DOM Slot Example</span>
</div>

<script>
  const slotHost = document.getElementById('slot-host');
  const shadowRoot = slotHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <style>
      .container {
        border: 1px solid #ccc;
        padding: 10px;
      }
    </style>
    <div class="container">
      <h1><slot name="title"></slot></h1>
      <p>This is a Shadow DOM component with a slot for the title.</p>
    </div>
  `;
</script>

Neste exemplo, o elemento <slot> é usado para passar conteúdo do light DOM para o Shadow DOM. O atributo slot no elemento span corresponde ao atributo name do elemento slot no Shadow DOM, permitindo que o conteúdo do span seja projetado no Shadow DOM.

Interação JavaScript com Shadow DOM

Interagir com o Shadow DOM via JavaScript requer compreender os limites do encapsulamento. A manipulação direta dentro do shadow root é simples, mas a interação externa requer atenção cuidadosa.

Acessando Elementos do Shadow DOM

Para acessar elementos dentro de um Shadow DOM, use a propriedade shadowRoot.

<div id="interactive-host"></div>

<script>
  const interactiveHost = document.getElementById('interactive-host');
  const shadowRoot = interactiveHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <button id="shadow-btn">Click me</button>
  `;

  const shadowButton = shadowRoot.querySelector('#shadow-btn');
  shadowButton.addEventListener('click', () => {
    alert('Button inside Shadow DOM clicked!');
  });
</script>

Neste exemplo, acessamos o botão dentro do Shadow DOM usando querySelector no shadow root. Como o shadow root é aberto, podemos anexar ouvintes de eventos e manipular elementos diretamente a partir do documento principal.

Redirecionamento de Eventos

Eventos que saem em bolha de uma shadow tree são redirecionados: para os ouvintes no documento externo, event.target aponta para o shadow host, não para o elemento interno que foi clicado. Isso mantém a estrutura interna privada. Dentro da shadow tree, o alvo real ainda está disponível através de event.composedPath()[0] ou event.target.

<div id="event-host"></div>

<script>
  const host = document.getElementById('event-host');
  const root = host.attachShadow({ mode: 'open' });
  root.innerHTML = '<button id="inner">Click me</button>';

  // Listener in the OUTER document
  document.addEventListener('click', (e) => {
    console.log('Outer target:', e.target.id || e.target.tagName);
    console.log('Real target:', e.composedPath()[0].id);
  });
</script>

Clicar no botão registra Outer target: event-host (redirecionado para o host), mas Real target: inner via composedPath(). Note que eventos personalizados só cruzam a fronteira do shadow quando criados com { bubbles: true, composed: true }.

Exemplos Práticos de Shadow DOM

Criando um Web Component Reutilizável

Criar um web component reutilizável usando Shadow DOM envolve definir um elemento personalizado e anexar um shadow root a ele.

<body>
  <custom-card title="Hello World"></custom-card>

  <script>
    class CustomCard extends HTMLElement {
      constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
          <style>
            .card {
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 5px;
              box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            }
            .card-title {
              font-size: 1.2em;
              margin-bottom: 5px;
            }
          </style>
          <div class="card">
            <div class="card-title">${this.getAttribute('title')}</div>
            <div class="card-content"><slot></slot></div>
          </div>
        `;
      }
    }

    customElements.define('custom-card', CustomCard);
  </script>
</body>

Neste exemplo, um elemento personalizado <custom-card> é criado com um Shadow DOM. O Shadow DOM encapsula os estilos e a estrutura do componente, tornando-o reutilizável sem se preocupar com conflitos de estilo com o documento principal. Combinar Shadow DOM com elementos personalizados e o elemento <template> é a receita padrão para Web Components em produção.

Integração com Frameworks

O Shadow DOM pode ser usado de forma transparente com frameworks JavaScript modernos como React, Angular e Vue.

Exemplo com React

No React, você pode anexar um Shadow DOM a um elemento contêiner da seguinte forma:

<body>
  <div id="root"></div>
  <!-- React and ReactDOM CDN links -->
  <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script type="text/babel">
    const { useRef, useLayoutEffect } = React;

    const CustomCard = ({ title, content }) => {
      const cardRef = useRef(null);

      useLayoutEffect(() => {
        if (cardRef.current) {
          const shadowRoot = cardRef.current.attachShadow({ mode: 'open' });

          shadowRoot.innerHTML = `
            <style>
              .card {
                padding: 10px;
                border: 1px solid #ddd;
                border-radius: 5px;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
              }
              .card-title {
                font-size: 1.2em;
                margin-bottom: 5px;
              }
            </style>
            <div class="card">
              <div class="card-title">${title}</div>
              <div class="card-content">${content}</div>
            </div>
          `;
        }
      }, [title, content]);

      return <div ref={cardRef}></div>;
    };

    const App = () => (
      <CustomCard title="Hello World" content="This is content inside the shadow DOM.">
      </CustomCard>
    );

    const rootElement = document.getElementById('root');
    const root = ReactDOM.createRoot(rootElement);
    root.render(<App />);
  </script>
</body>

Neste exemplo, um componente React CustomCard é criado e anexa um Shadow DOM a uma div comum. O Shadow DOM garante que os estilos e a estrutura do componente sejam encapsulados, proporcionando integração transparente com React.

Quando Usar Shadow DOM

O Shadow DOM não é obrigatório para todo componente, portanto avalie-o em relação às suas desvantagens:

  • Use-o quando você entrega um widget autossuficiente e reutilizável — especialmente um usado em páginas cujo CSS global você não controla (embeds, primitivos de design system, widgets de terceiros).
  • Evite-o quando seu componente vive inteiramente dentro de uma aplicação que já escopa estilos (CSS Modules, estilos com escopo, BEM) e você quer que a tematização global flua livremente.
  • Fique atento a estas armadilhas comuns:
    • Folhas de estilo globais e fontes não cascateiam automaticamente para dentro; declare o que você precisa dentro do root, ou passe valores via custom properties CSS (--my-color), que atravessam a fronteira.
    • Elementos associados a formulários precisam de configuração extra (a API ElementInternals) para participar de um <form> externo.
    • A renderização server-side de shadow trees requer Declarative Shadow DOM (<template shadowrootmode="open">).
Informação

Regra geral: prefira mode: 'open' e exponha ganchos de tematização com ::part() e custom properties CSS. Use closed apenas quando ocultar os internos for um requisito real.

Conclusão

Dominar o Shadow DOM é essencial para o desenvolvimento web moderno, proporcionando encapsulamento e reutilizabilidade poderosos. Ao compreender e implementar os conceitos e exemplos fornecidos, você pode criar componentes robustos e isolados que melhoram a manutenibilidade e a escalabilidade de suas aplicações web.

Este guia abrangente deve servir como base sólida para explorar e utilizar o Shadow DOM em seus projetos. Seja construindo widgets simples ou aplicações complexas, o Shadow DOM oferece o encapsulamento e a flexibilidade necessários para garantir que seus componentes permaneçam isolados e gerenciáveis.

Prática

Prática
Qual método é usado para criar um shadow root em JavaScript?
Qual método é usado para criar um shadow root em JavaScript?
Prática
O que host.shadowRoot retorna quando o root foi criado com mode: 'closed'?
O que host.shadowRoot retorna quando o root foi criado com mode: 'closed'?
Prática
Qual seletor permite que o documento externo estilize apenas as partes internas que um componente expõe explicitamente?
Qual seletor permite que o documento externo estilize apenas as partes internas que um componente expõe explicitamente?
Was this page helpful?