Testing shadow DOM

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.

Example 1: Open shadow DOM (custom element)

This web component uses attachShadow({ mode: 'open' }). Its shadow root is accessible via JavaScript.

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

Example 2: Closed shadow DOM (custom element)

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.

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

Example 3: Custom element, no shadow DOM

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.

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

Example 4: Open shadow DOM on a standard element

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.

HTML markup
<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 &lt;div&gt;. No custom element was used.</p>
  `;
</script>

Example 5: Closed shadow DOM on a standard element

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.

HTML markup
<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 &lt;div&gt;. No custom element was used.</p>
  `;
</script>

Example 6: Declarative shadow DOM

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.

HTML markup
<div id="declarative-host">
  <template shadowrootmode="open">
    <p>I am inside a declarative shadow DOM.</p>
  </template>
</div>

Example 7: Regular HTML, no shadow DOM

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.

I am a regular div element.
HTML markup
<div class="regular">I am a regular div element.</div>