javascripthtmlcsscustom-element

Is it possible to slot the style of a shadowRoot?


I'm experimenting with Vanilla custom elements and it doesn't seem possible to slot a style tag:

class MyElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: "open"});
  }
  connectedCallback() {
    this.shadowRoot.append(document.getElementById('my-element-template').content.cloneNode(true));
  }
}
window.customElements.define("my-element", MyElement);
<template id="my-element-template">
  <slot name="my-style">
    <style>
      p { color: red; }
    </style>
  </slot>
  <p>Why am I still red instead of green?</p>
</template>

<my-element>
  <style slot="my-style">
    p { color: green; }
  </style>
</my-element>


Solution

  • Update - Move style elements to ShadowDOM

    The original solution worked, but commenters suggested improvements. Style elements within a slot behave differently from other types elements. They appear in the DOM rather than the component's ShadowDOM. This is why OP's code did not work as expected. The original solution fixed this using shadowRoot.adoptedStyleSheets, but these steps were unnecessary. The simpler solution is to move the style elements directly into the ShadowDOM.

    Importantly, this only applies to styles added to slots and not to styles in the component definition.

    class MyElement extends HTMLElement {
      constructor() {
        super()
        this.attachShadow({ mode: "open" })
      }
      connectedCallback() {
        const root = this.shadowRoot
        root.append(
          document.getElementById("my-element-template").content.cloneNode(true),
        )
    
        // a fix specific to OP's code
        root.querySelector("slot[name=my-style]")
          .assignedElements()
          .forEach((el) => root.append(el))
    
        // and a longer but more universal alternative
        // this.importStyles();
      }
    
    
      // A method to import css from any or all slots
    
      /**
       * Imports slot style elements from lightDOM to shadowDOM
       * @param {string} [slotname] A slot name or omit for all slots
       */
      importStyles(slotname) {
        slotname = slotname ? `slot[name="${slotname}"]` : "slot"
        const root = this.shadowRoot
    
        root.querySelectorAll(slotname).forEach((slot) => {
          slot.assignedElements().forEach((el) => {
            if (el instanceof HTMLStyleElement) {
              root.append(el)
            } else {
              el.querySelectorAll("style").forEach((style) => root.append(style))
            }
          })
        })
      }
    }
    window.customElements.define("my-element", MyElement)
    p { 
      font-weight: bold;
      color: orange;
    }
    <p>This text is outside the component (global css)</p>
    
    <template id="my-element-template">
      <div>
      <slot name="my-style">
        <style>
          p { color: red; }
        </style>
      </slot>
      </div>
        <p>
          This text is <i>inside</i> the component (component css)
        </p>
    </template>
    
    <my-element>
      <style slot="my-style">
        p { color: green; } 
        p:hover { color: blue; }
        p i { color: black; } 
      </style>
    </my-element>

    Original Solution - works, but verbose

    class MyElement extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({mode: "open"});
      }
      connectedCallback() {
        const root = this.shadowRoot;
        root.append(document.getElementById('my-element-template').content.cloneNode(true));
    
        // get the slot style element
        const node = root.querySelector('slot[name=my-style]')
        .assignedNodes()[0];
    
        // extract the css and adopt custom style
        if (node instanceof HTMLStyleElement) {
          const stylesheet = new CSSStyleSheet();
          stylesheet.replace( node.textContent );
          root.adoptedStyleSheets = [stylesheet];
          node.remove();
        }
        
      }
    }
    window.customElements.define("my-element", MyElement);
    p { 
      font-weight: bold;
      color: orange;
    }
    <p>This text is outside the component (global css)</p>
    
    <template id="my-element-template">
      <slot name="my-style">
        <style>
          p { color: red; }
        </style>
      </slot>
        <p>
          This text is <i>inside</i> the component (component css)
        </p>
    </template>
    
    <my-element>
      <style slot="my-style">
        p { color: green; } 
        p:hover { color: blue; }
        p i { color: black; } 
      </style>
    </my-element>