36

I have a project where I'm using the shadow DOM natively (not through a polyfill). I'd like to detect if a given element is contained within a shadow DOM or a light DOM.

I've looked through all of the properties on the elements, but there don't seem to be any which vary based on the type of DOM an element is in.

How can I determine if an element is part of a shadow DOM or a light DOM?


Here is an example of what is considered "shadow DOM" and "light DOM" for the purpose of this question.

 (light root) • Document
      (light)   • HTML
      (light)   | • BODY
      (light)   |   • DIV
(shadow root)   |     • ShadowRoot
     (shadow)   |       • DIV 
     (shadow)   |         • IFRAME 
 (light root)   |           • Document
      (light)   |             • HTML
      (light)   |             | • BODY
      (light)   |             |   • DIV
(shadow root)   |             |     • ShadowRoot
     (shadow)   |             |       • DIV
       (none)   |             • [Unattached DIV of second Document]
       (none)   • [Unattached DIV of first Document]

<!doctype html>
<title>
  isInShadow() test document - can not run in Stack Exchange's sandbox
</title>
<iframe src="about:blank"></iframe>
<script>

function isInShadow(element) {
  // TODO
}

function test() {
  //  (light root) • Document
  //       (light)   • HTML
  var html = document.documentElement;

  console.assert(isInShadow(html) === false);

  //       (light)   | • BODY
  var body = document.body;

  console.assert(isInShadow(body) === false);

  //       (light)   |   • DIV
  var div = document.createElement('div');
  body.appendChild(div);

  console.assert(isInShadow(div) === false);

  // (shadow root)   |     • ShadowRoot
  var divShadow = div.createShadowRoot();

  var shadowDiv = document.createElement('div');
  divShadow.appendChild(shadowDiv);

  //      (shadow)   |       • DIV 
  console.assert(isInShadow(shadowDiv) === true);

  //      (shadow)   |         • IFRAME 
  var iframe = document.querySelector('iframe');
  shadowDiv.appendChild(iframe);

  console.assert(isInShadow(iframe) === true);

  //  (light root)   |           • Document
  var iframeDocument = iframe.contentWindow.document;

  //       (light)   |             • HTML
  var iframeHtml = iframeDocument.documentElement;

  console.assert(isInShadow(iframeHtml) === false);

  //       (light)   |             | • BODY
  var iframeBody = iframeDocument.body;

  //
  console.assert(isInShadow(iframeHtml) === false);

  //       (light)   |             |   • DIV
  var iframeDiv = iframeDocument.createElement('div');
  iframeBody.appendChild(iframeDiv);
   
  console.assert(isInShadow(iframeDiv) === false);
   
  // (shadow root)   |             |     • ShadowRoot
  var iframeDivShadow = iframeDiv.createShadowRoot();

  //      (shadow)   |             |       • DIV
  var iframeDivShadowDiv = iframeDocument.createElement('div');
  iframeDivShadow.appendChild(iframeDivShadowDiv);
    
  console.assert(isInShadow(iframeDivShadowDiv) === true);
     
  //        (none)   |             • [Unattached DIV of second Document]
  var iframeUnattached = iframeDocument.createElement('div');
    
  console.assert(Boolean(isInShadow(iframeUnattached)) === false);

  //        (none)   • [Unattached DIV of first Document]
  var rootUnattached = document.createElement('div');
    
  console.assert(Boolean(isInShadow(rootUnattached)) === false);
}

onload = function main() {
  console.group('Testing');
  try {
    test();
    console.log('Testing complete.');
  } finally {
    console.groupEnd();
  }
}

</script>
Jeremy
  • 1
  • 85
  • 340
  • 366

7 Answers7

37

If you call a ShadowRoot's toString() method, it will return "[object ShadowRoot]". According to this fact, here's my approach:

function isInShadow(node) {
    var parent = (node && node.parentNode);
    while(parent) {
        if(parent.toString() === "[object ShadowRoot]") {
            return true;
        }
        parent = parent.parentNode;
    }
    return false;
}

EDIT

Jeremy Banks suggests an approach in another style of looping. This approach is a little different from mine: it also checks the passed node itself, which I didn't do.

function isInShadow(node) {
    for (; node; node = node.parentNode) {
        if (node.toString() === "[object ShadowRoot]") {
            return true;
        }
    }
    return false;
}

function isInShadow(node) {
    for (; node; node = node.parentNode) {
        if (node.toString() === "[object ShadowRoot]") {
            return true;
        }
    }
    return false;
}

console.group('Testing');

var lightElement = document.querySelector('div');    

console.assert(isInShadow(lightElement) === false);

var shadowChild = document.createElement('div');
lightElement.createShadowRoot().appendChild(shadowChild);

console.assert(isInShadow(shadowChild) === true);

var orphanedElement = document.createElement('div');

console.assert(isInShadow(orphanedElement) === false);

var orphanedShadowChild = document.createElement('div');
orphanedElement.createShadowRoot().appendChild(orphanedShadowChild);

console.assert(isInShadow(orphanedShadowChild) === true);

var fragmentChild = document.createElement('div');
document.createDocumentFragment().appendChild(fragmentChild);

console.assert(isInShadow(fragmentChild) === false);

console.log('Complete.');
console.groupEnd();
<div></div>
Leo
  • 13,428
  • 5
  • 43
  • 61
  • Imo, he should've suggested changing the loop in a comment. An edit like that should only really be done by the person that posted the answer. – Cerbrus Dec 17 '14 at 07:50
  • So, that was an edit that actually changed the post's functionality. – Cerbrus Dec 17 '14 at 07:55
  • I generally endorse more aggressive editing than most Stack Overflow users: edit boldly, as Wikipedians say, while accepting the author's right to boldly revert everything you changed :P. However in this case I didn't intend to change the functionality in this post. I overlooked the difference -- my mistake! – Jeremy Dec 17 '14 at 19:58
  • Huh. I was hoping it would be possible to use `[Symbol.toStringTag]` directly instead of `.toString()` here, but it looks like that isn't implemented on ShadowRoots, even though the spec *seems* to suggest it should be. – Jeremy Mar 21 '16 at 20:32
14

You can check if an element has a shadow parent like this:

function hasShadowParent(element) {
    while(element.parentNode && (element = element.parentNode)){
        if(element instanceof ShadowRoot){
            return true;
        }
    }
    return false;
}

This uses instanceof over .toString().

Cerbrus
  • 70,800
  • 18
  • 132
  • 147
10

Element within shadowDOM can be found using getRootNode like below.

function isInShadow(node) {
   return node.getRootNode() instanceof ShadowRoot;
}
par
  • 817
  • 8
  • 21
8

⚠️ Warning: Deprecation Risk

The ::shadow pseudo-element is deprecated in and being removed from from the dynamic selector profile. The approach below only requires that it remain in the static selector profile, but it may also be deprecated and removed there in the future. Discussions are ongoing.

We can use Element's .matches() method to determine if an element is attached to a shadow DOM.

If and only if the element is in a shadow DOM, we will be able to match it by using the selector :host to identify elements that have a Shadow DOM, ::shadow to look in those shadow DOMs, and * and to match any descendant.

function isInShadow(element) {
  return element.matches(':host::shadow *');
}

function isInShadow(element) {
  return element.matches(':host::shadow *');
}

console.group('Testing');

var lightElement = document.querySelector('div');    

console.assert(isInShadow(lightElement) === false);

var shadowChild = document.createElement('div');
lightElement.createShadowRoot().appendChild(shadowChild);

console.assert(isInShadow(shadowChild) === true);

var orphanedElement = document.createElement('div');

console.assert(isInShadow(orphanedElement) === false);

var orphanedShadowChild = document.createElement('div');
orphanedElement.createShadowRoot().appendChild(orphanedShadowChild);

console.assert(isInShadow(orphanedShadowChild) === true);

var fragmentChild = document.createElement('div');
document.createDocumentFragment().appendChild(fragmentChild);

console.assert(isInShadow(fragmentChild) === false);

console.log('Complete.');
console.groupEnd();
<div></div>
Jeremy
  • 1
  • 85
  • 340
  • 366
  • 6
    Interestingly, [this document](http://dev.w3.org/csswg/css-scoping) shows the use of `:host::shadow`, although the description I see implies that any element that has a `::shadow` must necessarily be a `:host` which would make the pseudo-class quite redundant. I am not sure if it is actually necessary to specify `:host` for reasons I'm not understanding from the spec, or due to implementation limits. It also makes no mention of how `:root` would affect a shadow DOM, if at all, and neither does [Selectors 4](http://dev.w3.org/csswg/selectors-4/#the-root-pseudo) nor the Shadow DOM spec... – BoltClock Dec 15 '14 at 04:48
  • Is this answer still valid? – Fez Vrasta Jan 22 '20 at 08:38
5

Lets understand Light Dom:

The Light DOM is the user supplied DOM of an element that hosts a shadow root. For more info read at polymer-project.

https://www.polymer-project.org/platform/shadow-dom.html#shadow-dom-subtrees

This means: Light DOM is always relative to the next ancestor which hosts a shadow root.

An Element can be a part of the light dom of a custom element while it can be a part of the shadow root of another custom element at same time.

Example:

<my-custom-element>
    <shadowRoot>

        <custom-element>
            <div>I'm in Light DOM of "custom-element" and 
                    in Shadow Root of "my-custom-element" at same time</div>
        </custom-element>

    </shadowRoot>

    <div id="LDofMCE"> Im in Light DOM of "my-custom-element"</div>

<my-custom-element>

According to your question:

If you want to know if an element is in a shadow root, you just need to grab the element out of the document.

var isInLD = document.contains(NodeRef);
if(isInLD){
    console.alert('Element is in the only available "global Light DOM"(document)');
} else {
    console.log('Element is hidden in the shadow dom of some element');
}

The only Light DOM which is not preceeded by a shadow Root is part of the document, because Light DOM is relative as shown above.

It doesnt work backwards: if its part of the document its not in a Light DOM at all. You need to check if one of the ancestors is hosting a Shadow Root like suggested from Leo.

You can use this approach with other elements to. Just replace the "document" with e.g. "my-custom-element" and test if div#LDofMCE is in Light DOM relative to "my-custom-element".

Because of the lack of information about why you need this information i cant get closer...

EDIT:

It doesnt work backwards should be understand as follows:

Is this element in a Shadow Root?: document.contains() or the isInShadow(node) method from Leo deliver the answer.

"backwards" Question: Is this element in a Light DOM (In case you start searching relative to document)?: domcument.contains() does not deliver the answer because to be in a Light Dom - one of the elements ancestors needs to be a shadow host.

Come to the Point

  • Light DOM is relative.
  • An element can take part in a shadow root and in a light dom at the same time. there is no "is part of a shadow DOM OR a light DOM?"
Tobi
  • 184
  • 3
  • 1
    Create an element with `createElement` but not append to the document, that element also returns `false`. Is this what do you mean by **doesn't work backwards**? I didn't catch that sentence honestly. However, in some way a nice try :) – Leo Dec 17 '14 at 13:21
1

This can check if node or element is in shadow dom, and works for iframes

/**
 * @param {!Node} node
 * @return {!boolean}
 */
function isInShadowRoot(node) {
  return node.getRootNode().constructor.name === "ShadowRoot";
}

Or using instanceof

const inShadow = node.getRootNode() instanceof node.ownerDocument.defaultView.ShadowRoot
Zuhair Taha
  • 2,808
  • 2
  • 35
  • 33
0

You can check if any element is within a shadowDOM by doing:

/** element can be any HTMLELement or Node **/

const root = element && element.getRootNode();
const isOnShadowDOM = root instanceof ShadowRoot;
  • The first line tell us which is the root parent of any element. For example, a lightDOM element will have the root commonly as #document whiled a shadowed one will have the root as #shadow-root.

  • The second line tell us if this root is a shadowDOM. Here we simply check if the root element is a stance of ShadowRoot, if yes our const will be true, otherwise false.

Felippe Regazio
  • 189
  • 2
  • 4