
A Small Property for the Clonable Shadow Root
One boolean is all it takes to bridge the gap between encapsulated styles and the native cloneNode method, solving a decade-old limitation of Web Components.
A Small Property for the Clonable Shadow Root
I remember the first time I tried to build a dashboard using Web Components and a "drag to preview" feature. I grabbed my custom element, called const ghost = original.cloneNode(true), and appended it to the cursor. To my horror, the component was a hollow shell—no styles, no internal markup, just a lonely, empty host element. I spent an hour checking my CSS selectors before realizing the hard truth: the Shadow Root is a shy creature that, by default, refuses to be copied.
For over a decade, this was just "how things worked." If you wanted to clone a component with a Shadow Root, you had to manually re-attach the shadow and move the content over. It was clunky, error-prone, and felt like a massive oversight in the spec.
Then came the clonable property.
The "Empty Shell" Problem
Before we look at the fix, let's look at why this was so annoying. When you use element.cloneNode(true), the "true" signifies a deep clone. You expect everything inside that element to come along for the ride.
But Shadow DOM is encapsulated. The browser treated the Shadow Root as a private internal structure that shouldn't be touched by external methods—including cloning.
// The old behavior (which is still the default)
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `<p>I am invisible to clones!</p>`;
const clone = host.cloneNode(true);
console.log(clone.shadowRoot); // null. Sadness.If you were building a library that relied on cloning templates (like a virtual DOM implementation or a simple list renderer), you had to write weird workarounds to "re-hydrate" the shadow roots of every clone.
The One-Line Hero: clonable
The Web Components community eventually won the battle. We now have a boolean property in the attachShadow options called clonable. It defaults to false for backward compatibility, but setting it to true changes everything.
const host = document.createElement('div');
// This is where the magic happens
const shadow = host.attachShadow({
mode: 'open',
clonable: true
});
shadow.innerHTML = `<span>Now I travel with the host!</span>`;
const clone = host.cloneNode(true);
console.log(clone.shadowRoot.innerHTML); // "<span>Now I travel with the host!</span>"It’s such a small addition, but it bridges a massive gap. This makes Web Components behave much more like native HTML elements (like a <video> or <input>) which obviously don't lose their internals when you clone them.
What about Declarative Shadow DOM?
If you're using Declarative Shadow DOM (DSD) to render components from the server without JavaScript, you’re not left out. The HTML attribute is shadowrootclonable.
If you forget this attribute in your HTML, any client-side cloning of your SSR'd components will result in those same empty shells we talked about earlier.
<!-- Server-rendered component -->
<my-element>
<template shadowrootmode="open" shadowrootclonable>
<style>p { color: purple; }</style>
<p>I can be cloned from the server!</p>
</template>
</my-element>Why should you care?
You might think, *"I don't call cloneNode that often, why does this matter?"*
It matters because of the ecosystem. Many UI libraries, state management tools, and even some browser extensions rely on cloneNode to move fragments of the DOM around efficiently. If your component is "unclonable," it becomes a "black box" that breaks in those environments.
Practical Use Case: List Rendering
Imagine you have a complex Web Component acting as a table row. If you want to efficiently render 1,000 rows by cloning a template:
const templateRow = document.createElement('custom-row');
templateRow.attachShadow({ mode: 'open', clonable: true });
// ... set up shadow content ...
// Much faster than calling new CustomRow() 1,000 times
for (let i = 0; i < 1000; i++) {
const row = templateRow.cloneNode(true);
document.body.appendChild(row);
}Without clonable: true, every single one of those rows would be empty.
The Gotchas
There is always a "but," isn't there?
1. Browser Support: This is a relatively new addition (shipped in Chrome 124, Safari 17.4, and Firefox 123). If you're supporting older browsers, you'll still need a fallback or a polyfill.
2. References: Cloning the Shadow Root copies the structure, not the event listeners or the specific JavaScript references you might have held inside your class. You still need to handle logic initialization in your connectedCallback.
3. Closed Shadows: Even if you use mode: 'closed', you can still use clonable: true. The clone will also have a closed shadow root.
Wrapping Up
The clonable property is one of those "quality of life" updates that makes the web feel more cohesive. It removes a weird friction point that made Web Components feel like "second-class citizens" compared to built-in elements.
Next time you're setting up a Shadow Root, just tuck clonable: true in there. Your future self—the one trying to implement a drag-and-drop preview or a high-performance list—will thank you.


