How to load @font-face in dynamically loaded styles of Web Component with shadow DOM

Viewed 703

I have a HTML 5 WebComponent with shadow DOM which displays content that has to load styles depending on the content type that is displayed in the component. The list of stylesheets is fetched from the server.

I can load the stylesheets like this:

for (const style of styles) {
    const stylesheet = document.createElement('link');
    stylesheet.setAttribute('rel', 'stylesheet');
    stylesheet.setAttribute('href', style);
    stylesheet.setAttribute('type', 'text/css');
    this.root.appendChild(stylesheet);
}

However, the stylesheets sometimes also include @font-face rules, which are not added to the component. The browser never creates requests for the fonts references in the @font-face rules. How can I load these rules dynamically?

2 Answers

It turned out that browsers don't support loading @font-face CSS rules in shadow DOMs as of September 2020. The Chrome team seems to be working on this, though.

The approach I took was to look for @font-face rules in the dynamically created stylesheet, modify them a bit to get relative paths working and then add them to the <head> of the page with a script. If you add the @font-face rules to the head and the shadow DOM, the fonts will be loaded and applied.

Here's the code:

for (const style of styles) { // styles is an array of urls
    const stylesheet = document.createElement('link');
    stylesheet.setAttribute('rel', 'stylesheet');
    stylesheet.setAttribute('href', style);
    stylesheet.setAttribute('type', 'text/css');
    stylesheet.onload = (event) => {
        for (
            let i = 0;
            i < event.currentTarget.sheet.cssRules.length;
            i++
        ) {
            if (event.currentTarget.sheet.cssRules[i].type == 5) { // type 5 is @font-face
                const split = style.split('/');
                const stylePath = split
                    .slice(0, split.length - 1)
                    .join('/');
                let cssText =
                    event.currentTarget.sheet.cssRules[i].cssText;
                cssText = cssText.replace(
                    // relative paths
                    /url\s*\(\s*[\'"]?(?!((\/)|((?:https?:)?\/\/)|(?:data\:?:)))([^\'"\)]+)[\'"]?\s*\)/g,
                    `url("${stylePath}/$4")`
                );

                const st = document.createElement('style');
                st.appendChild(document.createTextNode(cssText));
                document
                    .getElementsByTagName('head')[0]
                    .appendChild(st);
            }
        }
    };
    this.root.appendChild(stylesheet);
}

It works fine on Edge 85 (Chromium based).

You can't do that but you can create a <style> element with JavaScript

Code from #524696

var css = 'h1 { background: red; }',
    head = document.head || document.getElementsByTagName('head')[0],
    style = document.createElement('style');

head.appendChild(style);

style.type = 'text/css';
if (style.styleSheet){
  // This is required for IE8 and below.
  style.styleSheet.cssText = css;
} else {
  style.appendChild(document.createTextNode(css));
}
Related