Theme editor won't work with declarative shadow dom

Short description of issue

Theme editor won’t work with declarative shadow DOM

Reproduction steps

Create a section with a minimal code using declarative shadow dom, such as:

<x-foo>
  <template shadowrootmode="open">
    <slot name="foo"></foo>
  </template>

  <p slot="foo">Hello</p>
</x-foo>

When editing a setting in the section, the theme editor will reload the section and apparenlty uses innerHTML to replace the section. However, this causes an issue when using declarative shadow dom.

Additional info

To imrpove support of declarative shadow dom, Shopify should use setHTMLUnsafe when available instead of innerHTML.

What type of topic is this

Bug report

Upload screenshot(s) of issue

Capture d’écran 2026-03-17 à 16.45.31.png

Hi @bakura10

Thanks for flagging - I’m digging into this now.

Thanks @Liam-Shopify

Due to how theme blocks don’t have information about their siblings, using shadow dom is the only way to create complex HTML with slots, so without proper support for Shadow DOM the experience for merchants will be extremely bad (in our next theme we’re using declarative shadow DOM extensively, so we would appreciate it to be supported).

For information @Liam-Shopify this also fails to work in the visual preview mode, which also prevents to have any kind of visual preview when using declarative shadow dom :(.

Hey @bakura10 ,

Sorry for the delay on this one. You’re right that the theme editor doesn’t currently preserve declarative shadow DOM when re-rendering sections inline.

There is a proven workaround: pair your declarative shadow DOM with an imperative fallback in your custom element. On initial page load, the browser’s HTML parser attaches the shadow root as expected. The fallback only kicks in when the component is re-mounted (e.g. during editor re-renders):

class DeclarativeShadowElement extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      const template = this.querySelector(':scope > template[shadowrootmode="open"]');
      if (template instanceof HTMLTemplateElement) {
        const shadow = this.attachShadow({ mode: 'open' });
        shadow.append(template.content.cloneNode(true));
      }
    }
  }
}

You can also see this pattern in action in our open-source Horizon theme.

I’m also curious about your use case. You mentioned your next theme uses declarative shadow DOM extensively, so understanding the following would be helpful:

  1. Are you also using the Section Rendering API to dynamically inject sections (e.g. for cart drawers or AJAX page transitions)? If so, how are you handling the HTML insertion on the client side?
  2. Is the editor re-render the main pain point, or are you hitting this in other contexts too?

Thanks

Hello,

I’m aware of this approach but it’s a bit annoying to have this on lot of sections. The main pain points is in the theme editor, because when we’re using section rendering API we’re using the new API (setHTMLUnsafe) when available. But in our use cases for our sections that use declarative shadow dom we don’t need to use section rendering API so the problem is only in the theme editor.

Considering that theme blocks don’t know of each other, the declarative shadow dom is the only viable approach to compose them so declarative shadow dom approach must be supported in the theme editor.

Hello @Luke-Shopify , is there any news on that? Theme editor is still not compatible with declarative shadow dom, which is now widely supported across all browsers.

Hey @bakura10 , sorry for the delay. There is some hesitance for us to use setHTMLUnsafeand other unsafe HTML methods internally, so we don’t have any plans to make a change at this time.

The suggested workaround is still to use the recommendation I mentioned in my previous comment, even though I understand that it is frustrating to have to repeat it across many sections

Great news then, there is a setHTML method (that you can use for modern browsers, then fallback to setHTMLUnsafe and then fallback to innerHTML): Element: setHTML() method - Web APIs | MDN