Dynamic Font Loading with FontFace

11 Aug 2021

There are a few use-cases where you'd like to load a completely different font and apply it to your styles. By load I mean, download/fetch the font resource file and use that font in your CSS or let's say you don't really know which font would be used in the page ahead of time.

That's where CSS Font Loading API comes in to help. It includes FontFace class that provides handy utilities to load dynamic fonts over network.

Let's consider this sample code below (I'm using Vue 3):

<template>
  <h1 :style="`font-family: ${selectedFont ?? 'inherit'};`">Hello World!</h1>
  <select v-model="selectedFont">
    <option
      :key="font.fontName"
      v-for="font in fontsList"
      :value="font.fontName"
    >
      {{ font.fontName }}
    </option>
  </select>
</template>

<script>
import { ref } from 'vue'
export default {
  name: 'App',
  setup() {
    let getFontsListFromAPI = () => {
      // TODO: get fonts list from API
      return [
        { fontName: 'Roboto', resourcePath: '..' },
        { fontName: 'Avenir', resourcePath: '..' },
      ]
    }
    let fontsList = getFontsListFromAPI()
    let selectedFont = ref(null)
    return { fontsList, selectedFont }
  },
}
</script>
<style>
/**/
</style>

Output: example font loading

Here we have a simple font selection dropdown which is v-model bound to a ref with name selectedFont. Based on the value of selectedFont we set the font-family style to our heading h1.

The values of the dropdown are font names that are obtained from an API endpoint. Since the list of fonts and their resource paths are dynamic we can't really use / define CSS styles that can accommodate for different font names unless you use some hack with binding font names and resource paths to the heading tags and using attr(..) url(..) etc.

Now to load dynamic fonts we can create a helper method in our setup as follows:

// inside setup()
// ..
let loadFont = async (fontName, url) => {
  let fontFace = new FontFace(fontName, `url(${url})`)
}
// ..

Here we create a new instance of FontFace with fontName and url as two parameters. The second parameter of the constructor must be either a URL or a binary font data. We can then use Vue's watch method to track changes of the dropdown which is v-modeled to selectedFont.

The FontFace class provides a load() method which actually loads the font and returns a Promise. Make sure to wrap it with the try/catch block. The value of selectedFont is the font name i.e String. So we'll have to get the original font object from our fonts list and call loadFont(..) using the font object (There a better ways of doing this).

// import { ref, watch } from "vue";
// inside setup()
//...
let loadFont = async (fontName, url) => {
  try {
    let fontFace = new FontFace(fontName, `url(${url})`)
    console.log(`Loading font: ${fontName}...`)
    await fontFace.load()
    document.fonts.add(fontFace)
  } catch (e) {
    console.error(e)
  }
}

watch(selectedFont, (fontName) => {
  let font = fontsList.find((f) => f.fontName === fontName)
  loadFont(font.fontName, font.resourcePath)
})
// ...

When a value in the dropdown is selected the selectedFont ref gets updated which is tracked by our watch that intern calls loadFont. On calling fontFace.load(), browser will fetch the font resource and add it to the Document Fonts list and since our heading tag (h1) has selectedFont named font family, it is rendered accordingly.

The FontFace constructor takes descriptors as an optional third argument where you can specify things like style, variant, weight of the font face.

Final Code:

<template>
  <h1 :style="`font-family: ${selectedFont ?? 'inherit'};`">Hello World!</h1>
  <select v-model="selectedFont">
    <option
      :key="font.fontName"
      v-for="font in fontsList"
      :value="font.fontName"
    >
      {{ font.fontName }}
    </option>
  </select>
</template>

<script>
import { ref, watch } from 'vue'
export default {
  name: 'App',
  setup() {
    let getFontsListFromAPI = () => {
      // TODO: get fonts list from API
      return [
        {
          fontName: 'Roboto',
          resourcePath: '..',
        },
        {
          fontName: 'Poppins',
          resourcePath: '..',
        },
      ]
    }

    let fontsList = getFontsListFromAPI()
    let selectedFont = ref(null)

    let loadFont = async (fontName, url) => {
      let fontFace = new FontFace(fontName, `url(${url})`)
      console.log(`Loading font: ${fontName}...`)
      await fontFace.load()
      document.fonts.add(fontFace)
    }

    watch(selectedFont, (fontName) => {
      let font = fontsList.find((f) => f.fontName === fontName)
      loadFont(font.fontName, font.resourcePath)
    })

    return { fontsList, selectedFont }
  },
}
</script>
<style>
#app {
  font-family: 'Times New Roman', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Final Result:

Further Reading / References: