Search code examples
cssweb-componentshadow-domnative-web-component

Why does my Web Component CSS not show? I am not using shadowDOM


I have a Native V1 component that is not using shadowDOM so I place my CSS in the <head>. But when someone else uses my component my CSS no longer works.

This only happens if their component does use shadowDOM.

Example Code for my component:

class MyEl extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    this.innerHTML = `<div class="spaced"><button class="happy-btn">I'm Happy</button></div>
    <div class="spaced"><button class="sad-btn">I'm Sad</button></div>`;
  }
}

// Define our web component
customElements.define('my-el', MyEl);
button {
  padding: 8px 20px;
}

.happy-btn {
  background-color: pink;
}

.sad-btn {
  background-color: #007;
  color: white;
}
<my-el></my-el>

My CSS is loaded into the <head> tag since I am not using shadowDOM. But once the outer element includes me in their shadowDOM then things fall apart.


Solution

  • If you are creating a component that does NOT use ShadowDOM that you may still need to add your CSS into a shadowRoot. If someone else places your component into their shadowDOM, then you must add your CSS to their shadowRoot. You can do this with the following code:

    const myStyle = document.createElement('style');
    myStyle.setAttribute('component', 'my-el');
    myStyle.textContent = `    button {
      padding: 8px 20px;
    }
    .happy-btn {
      background-color: pink;
    }
    
    .sad-btn {
      background-color: #007;
      color: white;
    }`;
    
    function addCss(el, selector, styleEl) {
      // Check to see if we have been placed into a shadow root.
      // If we have then add our CSS into that shadow root.
      let doc;
      try {
        doc = el.getRootNode();
        if (doc === document) {
          doc = document.head;
        }
      }
      catch(_ex) { doc = document.head; } // Shadow DOM isn't supported.
    
      if (!doc.querySelector(selector)) {
        doc.appendChild(styleEl.cloneNode(true));
      }
    }
    
    class MyEl extends HTMLElement {
      constructor() {
        super();
        addCss(this, 'style[component="my-el"]', myStyle);
      }
      connectedCallback() {
        this.innerHTML = `<div class="spaced"><button class="happy-btn">I'm Happy</button></div>
        <div class="spaced"><button class="sad-btn">I'm Sad</button></div>`;
      }
    }
    customElements.define('my-el', MyEl);
    
    class TheirEl extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({mode:'open'});
        this.shadowRoot.innerHTML = `<hr/><my-el></my-el><hr/><my-el></my-el><hr/>`;
      }
    }
    customElements.define('their-el', TheirEl);
    <their-el></their-el>

    The function addCss will place your CSS into the correct shadowRoot, or into document.head if there is no shadowRoot.

    You must call addCss within your constructor to place the CSS in the correct location. This routine will also make sure you don't add it twice as long as you have a unique selector to identify your <style> tag.

    In mine you see the <style> tag adds an attribute called component with a value of the component name. In my case component="my-el".

    Then I use the selector 'style[component="my-el"]' to see if that tag is already in the shadowRoot, or document.head if there is no shadowRoot, and only add the styles if it does not already exist.

    You can not assume that your component will not be in shadow DOM just because you are not using it. Use the example above to protect yourself.

    Side Note

    If you are using shadow DOM then this problem goes away since your have to place your CSS into your own shadowRoot.