37

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>

1

7 Answers 7

37
+300

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>

Sign up to request clarification or add additional context in comments.

4 Comments

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.
So, that was an edit that actually changed the post's functionality.
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!
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.
21

Element within shadowDOM can be found using getRootNode like below.

function isInShadow(node) { return node.getRootNode() instanceof ShadowRoot; } 

2 Comments

For modern browsers, this is the best answer, imo.
Somehow instanceof ShadowRoot does not do the job on my Chrome 120. This one works : return node.getRootNode().nodeType === Node.DOCUMENT_FRAGMENT_NODE && node.getRootNode().host !== undefined;
15

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().

4 Comments

however this may fail in iframe
Fail how? Can you provide a small example?
@Leo that link is broken
@try-catch-finally Nevermind, this post basically gives the same answer: window in the document and window in an iframe are not referencing the same object. So does the whole environment and native constructors.
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>

2 Comments

Interestingly, this document 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 nor the Shadow DOM spec...
Is this answer still valid?
6

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?"

1 Comment

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 :)
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 

Comments

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.