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.
| Aspecto | mode: 'open' | mode: 'closed' |
|---|---|---|
host.shadowRoot | Retorna o shadow root | Retorna null |
| Acesso externo | Permitido via host.shadowRoot | Apenas pela referência salva |
| Uso típico | A maioria dos componentes, depuração fácil | Ocultar internos de scripts da página |
| Inspeção no DevTools | Totalmente visível | Visível, mas mais difícil de acessar via script |
Estilização dentro do Shadow DOM
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 atributopart="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">).
- 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 (
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.