3

I have a web page that loads a very long list of custom web components, each with their own shadow DOM and a stylesheet shared by all instances.

Originally, I included the stylesheet as css file reference in a <link> tag to the code more readable:

(function () {
 const template = document.createElement('template');
 template.innerHTML = `
   <link rel='stylesheet' href='elements/myelement.css' />
   <div> ... </div>
 `;
 customElements.define('my-element', class extends HTMLElement {
  constructor() {...}
  connectedCallback() {...}
 }
})();

This has the unfortunate effect (I think) of the style loading after the content, and the element briefly appearing unstyled.

The problem is solved by pasting the styling between <style> tags within the innerHTML string, but the result looks unwieldy and difficult to read for a large style. One thing I tried was wrapping the value in an IIFE so the IDE could collapse it:

const style = (function(){ `...` })();
template.innerHTML = `
  <style>${style}<style>
  ...

Using the style tags, you then get a duplicate <style> string in each shadow DOM and I'm not sure if there's a performance penalty.

Is there an efficient and readable way to style custom web components?

2
  • In classic fashion, I found this page on Constructable Stylesheets which solves the problem in an elegant way. Unfortunately I couldn't get them to work in Chrome or Firefox (new CSSStyleSheet(); raises an Illegal constructor error). Maybe they aren't supported yet?
    – rovyko
    Feb 19, 2019 at 22:52
  • According to chromestatus, Constructable Stylesheets should be shipping with Chrome 73, which is the current Beta.
    – rovyko
    Feb 19, 2019 at 23:31

1 Answer 1

3

After reading through the Google walkthrough and testing on Chrome 73 Beta I've posted an answer on Stackoverflow and copied it below:

Constructable Stylesheets

This is a new feature that allows for the construction of CSSStyleSheet objects. These can have their contents set or imported from a css file using JavaScript and be applied to both documents and web components' shadow roots. It will be available in Chrome with version 73 and probably in the near future for Firefox.

There's a good writeup on the Google developers site but I'll summarize it briefly below with an example at the bottom.

Creating a style sheet

You create a new sheet by calling the constructor:

const sheet = new CSSStyleSheet();

Setting and replacing the style:

A style can be applied by calling the methods replace or replaceSync. replaceSync is synchronous, and can't use any external resources:

sheet.replaceSync(`.redText { color: red }`);

replace is asynchronous and can accept @import statements referencing external resources. Note that replace returns a Promise which needs to be handled accordingly.

sheet.replace('@import url("myStyle.css")')
  .then(sheet => {
    console.log('Styles loaded successfully');
  })
  .catch(err => {
    console.error('Failed to load:', err);
  });

Applying the style to a document or shadow DOM

The style can be applied by setting the adoptedStyleSheets attribute of either the document or a shadow DOM.

document.adoptedStyleSheets = [sheet]

The array in adoptedStyleSheets is frozen and can't me mutated with push(), but you can concatenate by combining with its existing value:

document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];

Inheriting from the document

A shadow DOM can inherit constructed styles from the document's adoptedStyleSheets in the same way:

// in the custom element class:
this.shadowRoot.adoptedStyleSheets = [...document.adoptedStyleSheets, myCustomSheet];

Note that if this is run in the constructor, the component will only inherit the style sheets that were adopted prior to its creation. Setting adoptedStyleSheets in the connectedCallback will inherit for each instance when it is connected. Notably, this will not cause an FOUC.

Example with Web Components

Let's create a component called x-card that wraps text in a nicely styled div.

// Create the component inside of an IIFE
(function() {
  // template used for improved performance
  const template = document.createElement('template');
  template.innerHTML = `
    <div id='card'></div>
  `;

  // create the stylesheet
  const sheet = new CSSStyleSheet();
  sheet.replaceSync(`
    #card {
      background-color: lavender;
      border: 1px solid #00000010;
      padding: 1em;
      border-radius: 10px;
      box-shadow: 2px 2px 5px #00000040;
      margin: 1em;
    }
  `);

  /*
  you can also call the stylesheet from a css file with:

  sheet.replace('@import url("xCardStyle.css")')
  .then(sheet => {
    console.log('Styles loaded successfully');
  })
  .catch(err => {
    console.error('Failed to load:', err);
  });
  */


  customElements.define('x-card', class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({
        mode: 'open'
      });
      // apply the HTML template to the shadow DOM
      this.shadowRoot.appendChild(
        template.content.cloneNode(true)
      );
      // apply the stylesheet to the shadow DOM
      this.shadowRoot.adoptedStyleSheets = [sheet];
    }

    connectedCallback() {
      const card = this.shadowRoot.getElementById('card');
      card.textContent = this.textContent;
    }
  });

})();
<x-card>Example Text</x-card>
<x-card>More Text</x-card>
1
  • I wish Firefox would hurry up an implement this. The bug is here. Aug 20, 2019 at 7:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.