Can fonts be imported from within the shadow DOM with a `<style>` tag?

69 Views Asked by At

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

There are 3 best solutions below

3
omar labidli On

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);
4
Danny '365CSI' Engelman On

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>

0
herrstrietzel On

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.