How we load fonts in our web project can make or break the user’s experience. With system fonts, we are guaranteed that text will render at the earliest possible time. But we may not desire these font types, and more importantly, they are not consistent between operating systems.
Web fonts, on the other hand, allow us to maintain a consistent look. Adding web fonts can be a simple task. However, it can also become tedious if we care about performance optimization.
One issue associated with web fonts that are hosted with font delivery services like Google Fonts is the cost of external network requests. While we can greatly reduce this incurred cost by self-hosting the fonts, we also have the issue of layout shift to deal with.
Next.js v13 introduced a font system called @next/font
to help abstract the complexity of optimizing fonts. This lesson covers how to use this font system to add custom and Google fonts in a Next.js project as well as to optimize the font loading experience.
Jump ahead:
- Adding fonts in Next.js
- How the
@next/font
system helps with font optimization in Next.js - Adding Google fonts with
@next/font
- Adding custom fonts in Next.js
- Adding fonts to Next.js with Tailwind CSS
- An issue with
display: optional
in the Chrome browser
See the demo project here. You can interact with the project to see how page elements render different fonts. Here is the project source code.
Now, let’s dive in.
Adding fonts in Next.js
Adding web fonts like Google Fonts in a Next.js application can be as simple as embedding <link>
tags generated from the font delivery service into the Head
component of the pages/_document.js
file.
Create your project
If you don’t have a Next.js project, start by creating one with the following command:
npx [email protected] nextjs-fonts cd nextjs-fonts
Then, run your build process with npm run dev
.
Now, add the following Home
component in the pages/index.js
file:
export default function Home() { return ( <div> <h1>Home page</h1> </div> ); }
Let’s implement a variable font called “Dancing Script” from Google Fonts by adding <link>
tags provided by the font delivery service into the Head
component of the pages/_document.js
file like so:
<Head> {/* preconnect scripts... */} <link href="https://fonts.googleapis.com/css2?family=Dancing+Script:[email protected]&display=swap" rel="stylesheet" /> </Head>
After that, we will update the font family in the styles/globals.css
:
body { /* ... */ font-family: 'Dancing Script', cursive; }
The output looks like so:
That is easy, right? Let’s see the problem with this implementation in the next section.
The font rendering process
Ideally, when a web font is applied to a web document, the browser will start downloading the font after parsing the HTML. This happens right before the text is rendered.
While we expect the font to load and render quickly, that is not always the case. It may take some extra seconds for the fonts to load, especially when it involves an external request, as is the case with Google Fonts.
See the request URL below:
So, pending the completion of the downloading font, the browser falls back to using the system font.
As we have seen with Google Fonts, the display=swap
parameter in the URL will trigger a font-display: swap
that instructs the browser to use a fallback font. After the web fonts has completely downloaded, it then swaps fonts.
That switching moment can cause a shift in the text layout when the font is changed. This effect is called a flash of unstyled text, otherwise called FOUT.
There are three strategies we could use to improve the loading experience:
- Eliminate the external network request by serving the fonts locally from our domain
- Use
font-display: optional
to prevent layout shift while also preloading the fonts - Use the CSS
size-adjust
property to reduce layout shifts when switching between fonts
Though we can manually implement these optimization strategies, we will take a look at how Next.js 13 makes the process easier with the @next/font
system.
How the @next/font
system helps with font optimization in Next.js
The @next/font
system is one of many powerful features introduced in Next.js 13. This font system significantly simplifies font optimization. It automatically self-hosts any Google Fonts by including them in deployment alongside other web components like HTML and CSS files.
Since no external request is involved by the browser, it results in a faster load time. It implements a strategy that prevents layout shift or significantly reduces the impact using the CSS size-adjust
property.
Install @next/font
The @next/font
comes as a separate package in Next.js so it doesn’t inflate the core. Let’s install it in our project:
npm install @next/font
Please ensure that you’re using Next.js v13 or newer.
Adding Google Fonts with @next/font
Web fonts, including those from Google Fonts, can either be variable fonts or non-variable fonts. A variable font has the advantage of integrating many variations of a typeface into a single file. Next.js recommends this type of font over the non-variable type. So, let’s start with that.
Adding font on a single page
Dancing Script from Google Fonts is a type of variable font. If you added this font to your project earlier when we went over the <link>
tags method, make sure you remove it before proceeding.
Now, let’s load Dancing Script using the @next/font
system. In the pages/index.js
file, let’s import the font from @next/font/google
as a function and create a new instance of it:
import { Dancing_Script } from '@next/font/google'; const dancingScript = Dancing_Script(); console.log(dancingScript); export default function Home() { return ( // ... ); }
In the code, we assigned the font instance to a dancingScript
variable and logged it to see what it returns. If we save the file and reload the page, we should see the returned object in the browser developer tools panel as well as the terminal:
The font loader returns an object containing options that we can use to apply the font style.
Applying font styles
As seen above, the returned object contains the className
property and a style
object that we can use to apply the font. Using the className
, we can apply a font to an element like so:
export default function Home() { return ( <div> <h1 className={dancingScript.className}>Home page</h1> </div> ); }
We can save the file and restart the dev server.
If we check the frontend, we should see the font applied. However, due to how Google Chrome behaves, a fallback font may apply instead.
For optimization purposes, every font has a font-display
property applied to it. By default, the value applied to fonts from the @next/font
system is font-display:optional
. This means if the font is not available within a short period, a fallback font will be used and no swapping will occur.
We could specify a value font-display:swap
to switch to the font as soon as it finishes loading, like so:
const dancingScript = Dancing_Script({ display: 'swap' });
This should work. However, in some cases, we might notice a slight shift.
For that reason, we will instead use the default display: optional
property while also preloading the font so we can omit the shift. This method produces the most effective result.
To preload the font, we must subset it to the required characters — for instance, latin
. So, let’s add a subset via the subsets
option:
const dancingScript = Dancing_Script({ subsets: ['latin'] });
This option will strip unrequired languages from the web font and ensures that a preload link for the required one is added in the <head>
element. If we save the file, we should see the injected link:
The preload hints to the browser that it should schedule the font for early download, and Next.js will ensure the resource is available to be used.
With this addition, the font should now also apply in the Chrome browser. The image below also shows that the request is within our infrastructure and not from a delivery service:
Bear in mind that sometimes, the default display: optional
in @next/font
can cause issues in the Chrome browser. Fonts applied may revert to fallback after navigating away until we manually refresh the chrome tab. We will discuss this issue later in this lesson and learn how to overcome it.
Adding multiple fonts
Adding multiple Next.js fonts using @next/font
is as simple as defining multiple font instances and applying the auto-generated classes to the document element:
import { Dancing_Script, Oswald } from '@next/font/google'; const dancingScript = Dancing_Script({ subsets: ['latin'] }); const oswald = Oswald({ subsets: ['latin'] }); export default function Home() { return ( <div className={oswald.className}> <h1 className={dancingScript.className}>Home page</h1> <p>This paragraph uses another font</p> </div> ); }
In the above code, we imported an Oswald
variable font from Google and applied it to the wrapper element to make it available for the children elements. The paragraph within the wrapper will inherit the font. Meanwhile, the font on the heading element will override the wrapper font.
See the output below:
Using the style
prop
We can also use the style
option returned from the font loader to apply font via the style
attribute. The following code uses the style
option to apply a font to the wrapper element:
export default function Home() { return ( <div style={oswald.style}> <h1 className={dancingScript.className}>Home page</h1> <p>This paragraph uses another font</p> </div> ); }
The above code will give us the same result as before.
Non-variable fonts
Unlike their variable counterparts, a non-variable font will require a weight
option. The weight
can be a string or an array of values.
An example of a non-variable font is Merriweather, which is also available through Google Fonts. Let’s start by importing it:
import { // ... Merriweather, } from '@next/font/google';
Then, add a required weight
option to the font instance and apply the generated class to the document element:
const merriweather = Merriweather({ weight: ['300', '400', '700'], style: ['normal', 'italic'], subsets: ['latin'], }); export default function Home() { return ( // ... <p className={merriweather.className}> This text uses a non variable font </p> // ... ); }
We’ve also added some optional style
values in the font instance. Save the file and the font should apply.
Adding a global font style
To apply a font throughout the entire web document, we will import the font in the pages/_app.js
file and pass the generated class to the Component
wrapper element, like so:
import { Raleway } from '@next/font/google'; const raleway = Raleway({ subsets: ['latin'] }); function MyApp({ Component, pageProps }) { return ( <div className={raleway.className}> <Component {...pageProps} /> </div> ); } export default MyApp;
In the code above, we imported a Raleway
font, which is also a variable font. So, adding a weight option in the configuration is optional.
Injecting fonts in the <head>
Another way to apply font across the document is by injecting the font in the <head>
. In the pages/_app.js
file, we can use the <style>
tag before the Component
to apply a global font:
function MyApp({ Component, pageProps }) { return ( <> <style jsx global>{` html { font-family: ${raleway.style.fontFamily}; } `}</style> <Component {...pageProps} /> </> ); } export default MyApp;
Remember, we have access to the font family from the style
object.
Declaring global font with CSS variables syntax
The syntax for CSS variables provides the flexibility of declaring multiple fonts in a stylesheet. The following code imports two different fonts in the pages/_app.js
file, which we then used to declare global CSS variables:
import { Raleway, IBM_Plex_Sans } from '@next/font/google'; const raleway = Raleway({ subsets: ['latin'] }); const ibmSans = IBM_Plex_Sans({ weight: '700', subsets: ['latin'], }); function MyApp({ Component, pageProps }) { return ( <> <style jsx global>{` :root { --raleway-font: ${raleway.style.fontFamily}; --ibmSans-font: ${ibmSans.style.fontFamily}; } `}</style> <Component {...pageProps} /> </> ); } export default MyApp;
The :root
pseudo-class in the code will select the root element — i.e., the <html>
.
Now, we can apply the CSS variables in a CSS file. The below code applies the variables in a styles/globals.css
file:
body { font-family: var(--raleway-font); } h1, h2, h3 { font-family: var(--ibmSans-font); }
The variable
key option
While creating a font instance, we can also add a variable
key that lets us declare a CSS variable. This option lets us easily use fonts with Tailwind CSS and CSS modules without using the global <style>
syntax.
The following code declares a CSS variable called --antonio-font
via the variable
key option:
const antonio = Antonio({ subsets: ['latin'], variable: '--antonio-font', });
If we log the result, we should see the returned object containing a variable
property that will let us access the font:
We can now apply the variable name via a className
attribute on a container element. If we declare a global font in the pages/_app.js
file, the Component
wrapper should look like so:
<div className={antonio.variable}> <Component {...pageProps} /> </div>
From here, we can use the font with Tailwind — as you will see later in the lesson — or with CSS modules. For CSS modules, let’s say we have an About
page component that uses this module, like so:
import styles from './About.module.css'; const About = () => { return ( <div> <h1>About page</h1> <p className={styles.text}>This is about page content</p> </div> ); }; export default About;
We can use the CSS variable that we declared earlier to access the font in the About.module.css
file, like so:
.text { font-family: var(--antonio-font); font-style: italic; }
Adding custom fonts in Next.js
In some scenarios, the font we want to use in Next.js may not be from Google Fonts. Instead, it could be a custom font that we created, a font that we bought, or one that we downloaded from another font site.
We can use any of these fonts locally by configuring them using the @next/font/local
. In this section, we will demonstrate how to do so by downloading a font called Cooper Hewitt from Font Squirrel.
The Cooper Hewitt font doesn’t come in the more modern and well-compressed .woff2
format. As a result, our first step is to convert it using the Font Squirrel Webfont Generator. After that, we will add the bold
and normal
weight variations of the font in the public/fonts
folder of our project:
Using the locally configured fonts
Like when using Google Fonts with Next.js, we can load the local fonts across the entire document or for a specific page. To load the local fonts sitewide, we will import the font loader from @next/font/local
in the pages/_app.js
file:
import localFont from '@next/font/local';
After that, we will define a new instance where we specify the src
of the local font files as an array of objects:
const cooper = localFont({ src: [ { path: '../public/fonts/cooperhewitt-book-webfont.woff2', weight: '400', }, { path: '../public/fonts/cooperhewitt-bold-webfont.woff2', weight: '700', }, ], });
Each of the objects represents the font for a specific weight. If we use a variable font, we can instead specify the src
as a string, like so:
const variableFont = localFont({ src: '../public/fonts/my-variable-font.woff2' });
From this point, how we apply the font styles is the same as that of the method used for Google Fonts discussed earlier. We can use either the className
syntax, style
, or CSS variables.
Let’s declare a CSS variable for the local font globally with the <style>
syntax:
function MyApp({ Component, pageProps }) { return ( <> <style jsx global>{` :root { /* ... */ --cooper-font: ${cooper.style.fontFamily}; } `}</style> <Component {...pageProps} /> </> ); }
We can now use the --cooper-font
CSS variable in the components stylesheet. Let’s say we have a Contact
page component, like so:
import styles from './Contact.module.css'; const contact = () => { return ( <div className={styles.contact}> <h1>Contact page</h1> <p>This is contact content uses a local font</p> </div> ); }; export default contact;
We can use the CSS variable to access the font in the Contact.module.css
file, like so:
.contact > *{ font-family: var(--cooper-font); }
Adding fonts to Next.js with Tailwind CSS
To use the @next/font
package with Tailwind CSS, we will use the CSS variable syntax. Fortunately, we already have declared a couple of CSS variables in the pages/_app.js
file.
Recall how earlier, we injected the following in the <head>
:
<style jsx global>{` :root { --raleway-font: ${raleway.style.fontFamily}; --ibmSans-font: ${ibmSans.style.fontFamily}; --cooper-font: ${cooper.style.fontFamily}; } `}</style>
We also defined a CSS variable name with the variable
option like so:
const antonio = Antonio({ subsets: ['latin'], variable: '--antonio-font', });
We can use any of these CSS variable declarations to add Next.js fonts with Tailwind CSS. Let’s see how in the next section.
Configuring tailwind.config.js
file
We will add the CSS variables as a font family in the Tailwind CSS configuration file:
const { fontFamily } = require('tailwindcss/defaultTheme'); /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}', ], theme: { extend: { fontFamily: { antonio: ['var(--antonio-font)', ...fontFamily.sans], ibm: ['var(--ibmSans-font)', ...fontFamily.sans], cooper: ['var(--cooper-font)', 'ui-serif', 'Georgia'], }, }, }, plugins: [], };
We can now use font-antonio
, font-ibm
, and font-cooper
utility class names to apply the fonts. A component that implements these utilities will look like so:
const Tailwind = () => { return ( <div> <h1 className="font-ibm">With Tailwind CSS</h1> <p className="font-antonio">This is a first paragraph</p> <p className="font-cooper">This is a second paragraph</p> </div> ); }; export default Tailwind;
An issue with display: optional
in the Chrome browser
You may encounter a Chrome issue with using the default display: optional
in a single page. This issue causes elements to revert to fallback fonts after navigating away to reload other pages. See the demo below:
As seen in the GIF above, the fonts applied to the home page elements reverted to fallback fonts after manually refreshing other pages. We had to again refresh the home page to reapply the proper web fonts.
One way to solve this issue is to use display: 'swap'
in the font instance instead of the default. However, as we already know, swapping the font could cause a noticeable layout shift.
What I recommend instead is moving the font instance into the pages/_app.js
file and using either the className
property, style
object, or CSS variables to apply the fonts on any page.
For our project, we will move the font declaration from the pages/index.js
file into the pages/_app.js
file and declare CSS variables for the font family, like so:
<style jsx global>{` :root { /* ... */ --oswald-font: ${oswald.style.fontFamily}; --merriweather-font: ${merriweather.style.fontFamily}; --dancingScript-font: ${dancingScript.style.fontFamily}; } `}</style>
We can now refactor the pages/index.js
file to use CSS modules, like so:
import styles from './Home.module.css'; export default function Home() { return ( <> <div className={styles.oswald}> <h1 className={styles.dancingScript}>Home page</h1> <p className={styles.merriweather}> This text uses a non variable font </p> <p>This paragraph uses another font</p> </div> <p>This text should take global styles</p> </> ); }
Then, use CSS variables that we declared to access the fonts in the Home.module.css
file, like so:
.oswald { font-family: var(--oswald-font); } .dancingScript { font-family: var(--dancingScript-font); } .merriweather { font-family: var(--merriweather-font); }
If we save all files, the fonts should now apply as expected on Chrome as they do in other browsers.
Conclusion
As we have seen in this lesson, the @next/font
system introduced in Next.js v13 simplifies font optimization by abstracting its complexity. We have used this system to add both custom and Google Fonts font families in a Next.js application.
In our demo project, we added multiple fonts to help demonstrate the various methods for using fonts in Next.js. However, in a production site, we should consider minimizing the number of fonts to preload.
I hope you enjoyed this lesson. If you have questions or contributions, share your thoughts in the comment section. See the complete source code for the project on GitHub.