Three Ways to Import External CSS in Web Components

A self-contained demo of three Shadow DOM styling strategies: CSS @import, asynchronous fetch, and constructable stylesheets with adoptedStyleSheets.

1. The @import Method

A <style> element is inserted into the Shadow DOM, and that style imports the external stylesheet.

Code:
class ImportRange extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });

    const node = document.createElement('style');
    node.textContent = `@import url("${rangeCssUrl}");`;
    shadow.append(node);

    shadow.append(createRangeMarkup('@import'));
    wireRange(shadow);
  }
}

if (!customElements.get('import-range')) {
  customElements.define('import-range', ImportRange);
}
Result:
@import showcase
stylesheet requested from Blob URL
This mirrors the article’s approach: the Shadow DOM receives a style tag whose only job is to import the CSS file. In this standalone page, the “external” file is represented by a generated Blob URL.

2. Fetching CSS with fetch

The component requests the CSS text, creates a style element, and appends that CSS into its Shadow DOM once the request resolves.

Code:
class FetchRange extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });

    shadow.append(createRangeMarkup('fetch'));
    wireRange(shadow);

    fetch(rangeCssUrl)
      .then(response => response.text())
      .then(css => {
        const node = document.createElement('style');
        node.textContent = css;
        shadow.append(node);
      });
  }
}

if (!customElements.get('fetch-range')) {
  customElements.define('fetch-range', FetchRange);
}
Result:
fetch showcase
waiting for CSS fetch
The markup can render immediately, then the fetched CSS is applied asynchronously. Move either thumb to see that the component behavior is the same as the first example.

3. Importing as a CSS Module

The modern target is to receive a stylesheet object and apply it to the Shadow DOM through adoptedStyleSheets.

Code:
// In a build or browser environment with CSS module support:
// import styles from './range.css' assert { type: 'css' };

const styles = new CSSStyleSheet();
styles.replaceSync(rangeCssText);

class ModuleRange extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });

    shadow.adoptedStyleSheets = [styles];
    shadow.append(createRangeMarkup('CSS module'));
    wireRange(shadow);
  }
}

if (!customElements.get('module-range')) {
  customElements.define('module-range', ModuleRange);
}
Result:
CSS module showcase
using adoptedStyleSheets
This self-contained demo creates the stylesheet object directly, which demonstrates the same Shadow DOM application step used by CSS module imports: assign a stylesheet to adoptedStyleSheets.

4. Comparison Summary

Each approach has one main tradeoff: performance, ease of use, or browser compatibility.

Code:
const methods = [
  { name: '@import', performance: false, easy: true,  compatible: true  },
  { name: 'fetch',   performance: true,  easy: false, compatible: true  },
  { name: 'import',  performance: true,  easy: true,  compatible: false }
];

document.querySelectorAll('[data-filter]').forEach(button => {
  button.addEventListener('click', () => {
    const key = button.dataset.filter;
    const rows = document.querySelectorAll('#comparison tbody tr');

    rows.forEach((row, index) => {
      row.hidden = key !== 'all' && !methods[index][key];
    });
  });
});
Result:
Interactive comparison
Method Good Performance Easy to Use Good Compatibility
@import No Yes Yes
fetch Yes No Yes
import Yes Yes No