skip to Main Content

Can a font be imported from within the shadow dom and have it apply to its children?

When trying to do something like

class MyCustomElement extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
      <style>
        @import url('https://fonts.googleapis.com/css?family=Open+Sans');
        :host {
          font-family: 'Open Sans';
        }
      </style>
      <h1>Hello font?</h1>
    `;
  }
}
customElements.define('my-custom-element', MyCustomElement);

The font isn’t being applied to the <h1>. The CSS inspector shows <h1>‘s font computed as "Open Sans". In the network tab, Chrome requests the stylesheet, but Firefox does not. In both browsers it renders with a system default font.

What’s missing to make this work?

The full HTML file is,

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Font test</title>
  </head>
  <body>
    <script>
      class MyCustomElement extends HTMLElement {
        constructor() {
          super();
          const shadowRoot = this.attachShadow({ mode: "open" });
          shadowRoot.innerHTML = `
      <style>
        @import url('https://fonts.googleapis.com/css?family=Open+Sans');
        :host {
          font-family: 'Open Sans';
        }
      </style>
      <h1>Hello font?</h1>
    `;
        }
      }
      customElements.define("my-custom-element", MyCustomElement);
    </script>
    <my-custom-element></my-custom-element>
  </body>
</html>

Firefox

Firefox example

Chrome

Chrome example

3

Answers


  1. When you use ‘@import’ within a Shadow DOM, it doesn’t always work as expected due to the timing of when the styles are applied.
    In your case, the ‘@import’ rule might not be applied in time for the font-family declaration .

    class MyCustomElement extends HTMLElement {
          constructor() {
           super();
           const link = document.createElement('link');
           link.rel = 'stylesheet';
           link.href = 'https://fonts.googleapis.com/css?family=Open+Sans';
           document.head.appendChild(link);
           const shadowRoot = this.attachShadow({mode: 'open'});
                        shadowRoot.innerHTML = `
                          <style>
                            :host {
                              font-family: 'Open Sans', sans-serif;
                            }
                          </style>
                          <h1>Hello font?</h1>
                        `;
                      }
                    }
      customElements.define('my-custom-element', MyCustomElement);
    
    Login or Signup to reply.
  2. You have to register an external Font both in the Global Scope and in the Web Component

    <script>
    customElements.define('my-element', class extends HTMLElement {
      constructor() {
      
        let href = "https://fonts.googleapis.com/css?family=Open+Sans";
    
        super().attachShadow({mode:'open'})
               .innerHTML = `<style>@import url('${href}')</style>` +
               `<style>:host{font-family:'Open Sans'}</style>` + 
               `<h1>Hello Font: </h1>`;
    
        // make sure font is globally loaded
        let fontExist = 
            //document.fonts.check("32px Open Sans");
            document.querySelector(`link[href="${href}"]`);
        if (!fontExist) {
          console.log("append LINK font");
          document.head.append(
            Object.assign(document.createElement("link"), {
              rel: "stylesheet",
              href,
              onload : () => this.fontloaded()
            }))
        } else {
          this.fontloaded();
        } 
      }
    
      fontloaded(){
        let h1 = this.shadowRoot.querySelector("h1");
        h1.innerHTML += getComputedStyle(h1).font;
        this.hidden = false;
        console.log(document.fonts.check('0px Open Sans'));
      }
    });
    </script>
    <my-element hidden></my-element>
    Login or Signup to reply.
  3. As a workaround you may also add a new font via CSS font loading API method FontFace() like so.

    h1 {
      font-family: "Open Sans";
      font-weight: 800;
      font-stretch: 75%;
    }
    <script>
      class MyCustomElement extends HTMLElement {
        constructor() {
          super();
          const shadowRoot = this.attachShadow({
            mode: "open"
          });
          //Define a FontFace
          const font = new FontFace("Open Sans", "url(https://fonts.gstatic.com/s/opensans/v40/mem8YaGs126MiZpBA-UFVZ0b.woff2)", {
            style: "normal",
            weight: "400",
            stretch: "75% 100%"
          });
          
          // wait for font 
          font.load().then( (loaded_face)=> {
            document.fonts.add(loaded_face)
            
            // append content
            shadowRoot.innerHTML = `
          <style>
            :host {
              font-family: 'Open Sans';
              font-weight: 300;
              font-stretch: 75%;
            }
          </style>
          <h1>Hello font?</h1>
        `;
          }).catch((error)=>{});
        }
      }
      customElements.define("my-custom-element", MyCustomElement);
    </script>
    <my-custom-element></my-custom-element>
    
    <hr>
    
    <h1>Title in parent document</h1>

    Keep in mind, this method actually adds the font to the global scope – see 2nd heading in example also using "Open Sans".

    This approach also requires to extract the actual font file URL from the google fonts API query.

    However, we can quite reliably check if/when the font is loaded – which is handy for many rendering tasks e.g drawing text with a custom font to canvas.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search