Dynamic font loading with FontFace
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: