How to Actually Remove, Replace, or Clone Shadow DOM Roots

Updated

8 min read

How to Actually Remove, Replace, or Clone Shadow DOM Roots

Introduction

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.

👎 Reasons Not to Remove, Replace, or Clone Shadow 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:

  • "to prevent encapsulated styles from being manipulated by developers or content folks using your web component"
  • "to secure sensitive code from being exposed to consumers"
  • "in order to support light DOM [...] for whatever reason"

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

👍 Valid Reasons to Remove, Replace, or Clone Shadow Root

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:

  • Reinitializing components: resetting a component with new props or state
  • Dynamic component replacement: swapping out entire component instances
  • Managing multiple instances: creating or removing multiple instances of a component in the same container
  • Avoiding Shadow DOM conflicts: in cases where you need to ensure a clean slate for each component initialization
  • Implementing undo/redo functionality: such as reverting to previous component states

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.

Removing a Shadow DOM Root

So you've attached a Shadow DOM and now want to remove it? Maybe you have something like this:

index.html
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:

index.tsx
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").

index.tsx
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.

Replacing a Shadow DOM Root

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.

index.tsx
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:

Old Method of Replacing a Shadow Root

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:

Replaced Shadow Root

So while the old method doesn't work anymore, you can use the wrapper method as an easy and robust workaround.

Cloning a Shadow DOM Root

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.

Cloned Shadow Root

Important

This 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.

index.tsx
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.

Summary

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.

Ryan Chiang

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:

2024

2023

© 2023 Ryan Chiang|ryanschiang.com