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:
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: