Updated
—
8 min read
I came across this article by Jen Chan, where the author lists 5 reasons why you should never try to clone, close, or disable a Shadow DOM root.
The article's reasons are valid, but the supposed motivations for wanting to do this were common misconceptions about Shadow DOM that the author points out:
These are common misconceptions about Shadow DOM, not valid motivations for removing, replacing, cloning, or disabling a shadow root:
A main selling point of the Shadow DOM is to encapsulate styles, the Shadow DOM is not a security feature of any sort, so expecting it to "secure sensitive code" is completely misguided, and attempting to "support light DOM" while relying on Shadow DOM is self-explanatory I'd hope...
So, all in all, I agree with the author. If these are your motivations for wanting to remove, replace, clone, or "disable" a Shadow DOM, then don't do it.
But who actually has these motivations?
There are plenty of valid reasons to want to remove, replace, or clone a Shadow DOM root, such as:
You might be facing the error like:
Uncaught DOMException: Element.attachShadow: Unable to re-attach to existing ShadowDOM
In this short guide, I'm going to share how to actually replace, remove, or clone a Shadow DOM root.
So you've attached a Shadow DOM and now want to remove it? Maybe you have something like this:
html<html>
<body>
<div id="shadow-root">
#shadow-root
<div>
<!-- Component goes here -->
</div>
</div>
</body>
</html>
And here's how we added the Shadow DOM in React:
javascriptconst targetElement = document.getElementById("shadow-root");
// Create a shadow root
const shadowRoot = targetElement.attachShadow({ mode: "open" });
// Create a container for your React app inside the shadow DOM
const reactContainer = document.createElement("div");
shadowRoot.appendChild(reactContainer);
const root = createRoot(reactContainer);
root.render(
<App />
)
While browser APIs do not support removing a shadow root that is already attached, you can use a "wrapper method."
Instead of removing the shadow root itself, we remove the element we attached it to (i.e. its "wrapper").
javascriptconst targetElement = document.getElementById("shadow-root");
let wrapper = targetElement.querySelector(".shadow-wrapper");
if (wrapper) {
// If wrapper exists, remove it to remove the Shadow DOM root
targetElement.removeChild(wrapper);
}
By removing the .shadow-wrapper
, we can effectively remove the Shadow DOM root without violating any web component standards.
So you've attached a Shadow DOM and now want to replace it? We can use a similar wrapper approach as we used for removing the shadow root.
javascriptlet wrapper = targetElement.querySelector(".shadow-wrapper");
if (wrapper) {
// If wrapper exists, remove it to remove the existing Shadow DOM
targetElement.removeChild(wrapper);
}
// Re-attach the new Shadow DOM
wrapper = document.createElement("div");
wrapper.className = "shadow-wrapper";
targetElement.appendChild(wrapper);
// Create a shadow root
const shadowRoot = wrapper.attachShadow({ mode: "open" });
This is pretty much identical to removing the shadow root, and also works for reinitializing a shadow DOM root.
Let's quickly examine why the "old method" of replacing a shadow root doesn't work anymore:
As mentioned in this thread, you also used to be able to replace a shadow root by simply attach another shadow root to override the previous one.
As shown in this documentation, when multiple shadow trees are attached to a host, rendering follows a last-in, first-out (LIFO) method.
javascriptconst targetElement = document.getElementById('shadow-root');
const oldRoot = targetElement.createShadowRoot();
const newRoot = targetElement.createShadowRoot();
oldRoot.innerHTML = '<div>Old root to be replaced</div>';
newRoot.innerHTML = '<div>New root to do replacing</div>';
So in this example, the newRoot
would take precedence.
But the createShadowRoot
method is deprecated in favor of the attachShadow
method.
If you attempt to use createShadowRoot
you'll get createShadowRoot is not a function
.
So let's try and replace it with attachShadow
like so:
javascriptconst targetElement = document.getElementById('shadow-root');
const oldRoot = targetElement.attachShadow({ mode: "open" });
const newRoot = targetElement.attachShadow({ mode: "open" });
oldRoot.innerHTML = '<div>Old root to be replaced</div>';
newRoot.innerHTML = '<div>New root to do replacing</div>';
But now we'll face the error:
Failed to execute 'attachShadow' on 'Element': Shadow root cannot be created on a host which already hosts a shadow tree.
So instead we need to use the wrapper method described above:
javascript const targetElement = document.getElementById('shadow-root');
// Attach first shadow root
let wrapper = document.createElement("div");
wrapper.className = "shadow-wrapper";
targetElement.appendChild(wrapper);
const oldRoot = wrapper.attachShadow({
mode: "open"
});
oldRoot.innerHTML = '<div>Old root to be replaced</div>';
// Replace and attach new shadow root
wrapper = targetElement.querySelector(".shadow-wrapper");
if (wrapper) {
targetElement.removeChild(wrapper);
}
wrapper = document.createElement("div");
wrapper.className = "shadow-wrapper";
targetElement.appendChild(wrapper);
const newRoot = wrapper.attachShadow({
mode: "open"
});
newRoot.innerHTML = '<div>New root to do replacing</div>';
And it works! You can see in the browser inspector the new root:
So while the old method doesn't work anymore, you can use the wrapper method as an easy and robust workaround.
Lastly, you may want to clone a shadow root.
We might first attempt to use cloneNode
like so:
javascriptconst oldRoot = wrapper.attachShadow({
mode: "open"
})
// Cannot use cloneNode on shadow root
const newRoot = oldRoot.cloneNode(true);
But this throws the error Operation is not supported
. So we might then attempt to clone the wrapper element by using cloneNode
on the wrapper element instead:
javascriptconst newWrapper = wrapper.cloneNode(true);
targetElement.appendChild(newWrapper);
But in this case, we only clone the wrapper and not the shadow root attached to it. So what's the solution to cloning a shadow root?
Here's a naive workaround to cloning a shadow root:
javascript// Attach first shadow root
let wrapper = document.createElement("div");
wrapper.className = "shadow-wrapper";
targetElement.appendChild(wrapper);
const oldRoot = wrapper.attachShadow({
mode: "open"
});
oldRoot.innerHTML = '<div>Clone me!</div>';
// Naively clone the shadow root
const rootToClone = wrapper.shadowRoot;
const newWrapper = document.createElement("div");
newWrapper.className = "shadow-wrapper";
targetElement.appendChild(newWrapper);
const newRoot = newWrapper.attachShadow({
mode: rootToClone.mode,
});
newRoot.innerHTML = rootToClone.innerHTML;
To clone the shadow root, we attach a new shadow root and copy the innerHTML
of the original shadow root.
ImportantThis will not clone and transfer any event listeners (which is tricky and you may need a more sophisticated approach depending on your use case). This will also not clone any adoptedStyleSheets, although that is possible.
This method of cloning a shadow root works, but is naive. Ideally you have a React component, like <App />
which makes "cloning" robust and maintainable.
We're not truly cloning, but creating a new instance with same initial state/props and leveraging a component-based approach.
javascript// Attach first shadow root
const originalWrapper = document.createElement("div");
originalWrapper.className = "shadow-wrapper";
targetElement.appendChild(originalWrapper);
const originalShadowRoot = originalWrapper.attachShadow({
mode: "open"
});
const originalReactContainer = document.createElement("div");
originalShadowRoot.appendChild(originalReactContainer);
const originalReactRoot = createRoot(originalReactContainer);
originalReactRoot.render(
<App cloned={false} />
);
// Clone using the <App> component
const clonedWrapper = document.createElement("div");
clonedWrapper.className = "shadow-wrapper";
targetElement.appendChild(clonedWrapper);
const clonedShadowRoot = originalWrapper.attachShadow({
mode: originalShadowRoot.mode,
});
const clonedReactContainer = document.createElement("div");
clonedShadowRoot.appendChild(clonedReactContainer);
const clonedReactRoot = createRoot(clonedReactContainer);
clonedReactRoot.render(
<App cloned={true} />
);
While the example uses React, this component-based approach to 'cloning' works equally well, if not better, with Web Components.
Since Shadow DOM is a key part of the Web Components specification, using custom elements to manage your shadow roots can provide a more native and potentially more efficient solution.
While there are many reasons not to remove, replace, or clone a shadow root, there are also valid reasons.
In this article, I shared 3 methods of removing, replacing, and cloning a shadow root using the "wrapper method".
The main takeaway is that you cannot (and should not attempt to) directly remove the shadow root element itself. Instead, you can attach the shadow root to a wrapper element which you can remove and replace.
Cloning a shadow root is trickiest, and while it can be done naively, ideally you have component you can reinitialize instead of manually copying child nodes, adopted style sheets, and event listeners.
Lastly, note that while its feasible and perfectly fine to replace, remove, and clone Shadow DOM roots, recreating shadow roots may have performance implications, such as complex components or done at a high frequency.
Meet the Author
Ryan Chiang
Hello, I'm Ryan. I build things and write about them. This is my blog of my learnings, tutorials, and whatever else I feel like writing about.
See what I'm building →.
Thanks for reading! If you want a heads up when I write a new blog post, you can subscribe below: