Which of the examples below use shadow DOM, and can JavaScript detect them?
Important note for auditors: shadow DOM is not limited to custom elements (hyphenated tag names). A fixed set of standard elements — including <div>, <span>, <article>, <section>, and several others — can also host a shadow root via attachShadow(). A detection method that only checks for hyphenated tag names will miss shadow DOM attached to standard elements entirely. A more reliable check tests element.shadowRoot !== null across all elements on the page, not just custom ones, though this still cannot detect a closed shadow root, only that an open one exists.
This web component uses attachShadow({ mode: 'open' }). Its shadow root is accessible via JavaScript.
<open-component></open-component>
<script>
class OpenComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<p>I am inside an <strong>open</strong> shadow DOM.
JavaScript can access my shadow root.</p>
`;
}
}
customElements.define('open-component', OpenComponent);
</script>
This web component uses attachShadow({ mode: 'closed' }). Its shadow root is intentionally hidden from JavaScript. A bookmarklet can see the custom element but cannot confirm or access the shadow DOM.
<closed-component></closed-component>
<script>
class ClosedComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<p>I am inside a <strong>closed</strong> shadow DOM.
JavaScript cannot access my shadow root.</p>
`;
}
}
customElements.define('closed-component', ClosedComponent);
</script>
This is a custom element (hyphenated tag name) with no shadow DOM attached at all. A bookmarklet relying only on element.shadowRoot flags it the same way as closed shadow DOM — it can see the element but cannot confirm what's inside.
<no-shadow-component></no-shadow-component>
<script>
class NoShadowComponent extends HTMLElement {
constructor() {
super();
this.innerHTML = `<p>I am a custom element with no shadow DOM at all.</p>`;
}
}
customElements.define('no-shadow-component', NoShadowComponent);
</script>
This is a plain <div>, not a custom element, with attachShadow({ mode: 'open' }) called on it directly. <div> is one of a fixed set of standard elements that support shadow DOM. A bookmarklet that only checks for hyphenated tag names will miss this entirely. One that checks element.shadowRoot !== null on every element will correctly detect it.
<div id="standard-open-host"></div>
<script>
const host = document.getElementById('standard-open-host');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<p>I am inside an <strong>open</strong> shadow DOM attached
to a plain <div>. No custom element was used.</p>
`;
</script>
Same idea as Example 4, but closed. This is the hardest case for a bookmarklet: no hyphenated tag name to spot, and no accessible shadow root to confirm. There is currently no reliable way for page-level JavaScript to detect this case from the outside.
<div id="standard-closed-host"></div>
<script>
const host = document.getElementById('standard-closed-host');
const shadow = host.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<p>I am inside a <strong>closed</strong> shadow DOM attached
to a plain <div>. No custom element was used.</p>
`;
</script>
This shadow root is created by the HTML parser itself, using a <template shadowrootmode="open">, with no JavaScript involved at all. After parsing, the <template> is replaced by an actual shadow root attached to its parent. This is increasingly used in server-rendered web components. Support for this pattern is relatively recent (Chrome 111+, Firefox 123+, Safari 16.4+), so worth confirming current browser support before relying on it for testing.
I am inside a declarative shadow DOM. No JavaScript created me.
<div id="declarative-host">
<template shadowrootmode="open">
<p>I am inside a declarative shadow DOM.</p>
</template>
</div>
This is a standard HTML element with no shadow DOM and no custom element. Any reliable detection method, hyphenated-tag-based or shadowRoot-based, will correctly not flag it.
<div class="regular">I am a regular div element.</div>