17

What is the canonical way to import styles into a web component?

The following gives me an error HTML element <link> is ignored in shadow tree:

<template>
    <link rel="style" href="foo.css" />
    <h1>foo</h1>
</template>

I am inserting this using shadow DOM using the following:

var importDoc, navBarProto;

importDoc = document.currentScript.ownerDocument;

navBarProto = Object.create(HTMLElement.prototype);
navBarProto.createdCallback = function() {
  var template, templateClone, shadow;

  template = importDoc.querySelector('template');
  templateClone = document.importNode(template.content, true);

  shadow = this.createShadowRoot();
  shadow.appendChild(templateClone);
};

document.registerElement('my-nav-bar', {
  prototype: navBarProto
});
Ben Aston
  • 53,718
  • 65
  • 205
  • 331

6 Answers6

20

Now direct <link> tag is supported in shadow dom.

One can directly use:

<link rel="stylesheet" href="yourcss1.css">
<link href="yourcss2.css" rel="stylesheet" type="text/css">  

It has been approved by both whatwg and W3C.

Useful links for using css in shadow dom:

Direct css link can be used in shadow dom.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
Himanshu sharma
  • 7,487
  • 4
  • 42
  • 75
  • 3
    This is not working for me when I'm importing a stylesheet from an external site. Is this prohibited? `` – Jaap Weijland Mar 17 '21 at 16:50
  • What is the browser support of this? I can't find on caniuse. – Miriam Zusin Sep 11 '22 at 18:23
  • 1
    Although this works, please note the browser will download your stylesheet everytime you create an instance of your custom web component, check the network tab of dev tools to confirm. – run_the_race Sep 22 '22 at 10:02
14

If you need to place external styles inside the <template> tag you could try

<style> @import "../my/path/style.css"; </style>

however I have a feeling this will start importing after the element has been created.

Jammer
  • 1,548
  • 20
  • 37
  • "however I have a feeling this will start importing after the element has been created." I'm happy you bring this up -- this can be a problem when importing with `` where the import will occur after web component creation, and styles will "blink" into place. Not sure if that's true for `@import` as well. – rpivovar Jan 14 '22 at 15:20
  • From my experience both `import` and `link` cause the stylesheet to be re-downloaded every time a component is created (check the network tab of dev tools) to confirm. – run_the_race Sep 22 '22 at 09:47
12

Answer no longer valid

The @import syntax was removed from CSSStyleSheet.replace()

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 be 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();
  // set its contents by referencing a file
  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>
rovyko
  • 4,068
  • 5
  • 32
  • 44
  • 2
    i do not see any tracking on caniuse.com on that. What is the Firefox support for that? So far this looks like a Chrome only tech right? – Thomas Jul 13 '19 at 07:20
  • @Thomas - These are now implemented by Firefox https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet – rovyko Aug 09 '22 at 15:57
  • As usual Safari lagging behind, no `replace` or `replaceSync` support yet – run_the_race Sep 22 '22 at 09:53
  • `@import` support has been deprecated for `replace` and `replaceSync`: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/replace#parameters So this answer no longer works. – run_the_race Nov 21 '22 at 12:11
  • @run_the_race - this is correct, the answer is no longer valid as I do not see an alternative method for importing external stylesheets – rovyko Nov 21 '22 at 12:48
  • It's unfortunate because it was looking like the way to do. Maybe we are supposed to perform a fetch on the CSS and put the text of the response into `replace()`, but that would not be a cached solution. – run_the_race Nov 21 '22 at 13:36
2

NB!!!

THIS ANSWER IS OUTDATED

PLEASE CHECK THE ANSWER BY Himanshu Sharma

Up-to-date answer: https://stackoverflow.com/a/48202206/2035262

According to Polymer documentation:

Polymer allows you to include stylesheets in your <polymer-element> definitions, a feature not supported natively by Shadow DOM.

This is a bit weird reference, but I could not google the straight one. It looks like at the moment there is no rumors about supporting links inside templates.

That said, whether you want to use vanilla web component, you should either inline your css with <style> tag, or load and apply your css manually in javascript.

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
1

The above answers show how to import stylesheets into a web component, but importing a single style to a shadow DOM can be done (kind-of) programmatically. This is the technique I developed recently.

First - make sure that you embed your component-local styles directly in a template with the HTML code. This is to make sure that the shadow DOM will have a stylesheet in your element constructor. (importing other stylesheets should be ok, but you must have one ready in the constructor)

Second - use a css-variable to point at a css rule to import.

#rule-to-import {
   background-color: #ffff00;
}

my-element {
   --my-import: #rule-to-import;
}

Third - In the component constructor, read the CSS variable and locate the pointed to style in the document stylesheets. When found, copy the string but rewrite the selector to match the internal element(s) you wish to style. I use a helper function for this.

importVarStyle(shadow,cssvar,target) {
    // Get the value of the specified CSS variable
    const varstyle=getComputedStyle(this).getPropertyValue(cssvar).trim();
    if(varstyle!="") varstyle: {
        const ownstyle=shadow.styleSheets[0];
        for(let ssheet of document.styleSheets) {   // Walk through all CSS rules looking for a matching rule
            for(let cssrule of ssheet.cssRules) {
                if(cssrule.selectorText==varstyle) {    // If a match is found, re-target and clone the rule into the component-local stylesheet
                    ownstyle.insertRule(
                        cssrule.cssText.replace(/^[^{]*/,target),
                        ownstyle.cssRules.length
                    );
                    break varstyle;
                }
            }
        }
    }
}
-3

Try the <style> element inside of <template>:

<template>
    <style>
       h1 { 
         color: red;
         font-family: sans-serif;
       }
    </style>
    <h1>foo</h1>
</template>
markg
  • 424
  • 3
  • 10