Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

Next.js font optimization: Adding custom and Google fonts

12 min read 3635 109

Next Js Font Optimization Adding Custom And Google Fonts

Editor’s note: This article was updated on 30 March 2023 to include information on FOIT (flash of invisible text), reusing fonts, and relevant details (pertaining to Google Fonts) on improving privacy for sites subject to GDPR.

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 article covers how to use this font system to add custom fonts and Google Fonts in a Next.js project as well as to optimize the font loading experience.

Jump ahead:

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 Headcomponent of the pages/_document.js file.

Creating 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 Headcomponent 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 file:

body {
  /* ... */
  font-family: 'Dancing Script', 'Helvetica Neue', cursive;
}

The output looks like this:

Browser Opened To Localhost In Light Mode With Text In Dancing Script Font Reading Home Page

That is easy, right? Let’s see the problem with this implementation in the next section.

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

Browser Developer Pane Open In Dark Mode With Red Arrow Pointing To External Request Url For Font Rendering

So, pending the completion of the downloading font, we need to tell the browser how to behave with regard to the text content using the font-display CSS descriptor.



By providing a value of swap, as we have seen with Google Fonts (the display=swap parameter in the URL will trigger a font-display: swap), we instruct the browser to render the page immediately with a fallback font and then swap the font with the web font after it has completely loaded.

This however triggers a flash of unstyled text, otherwise called FOUT. That switching moment can cause a shift in the text layout when the font is changed. If we simulate a font request on a slower connection, the FOUT will behave like so:

FLOUT Flash Of Unstyled Text

Apart from the value of swap, other possible values for font-display include auto, block, fallback, and optional. With a value of block, we tell the browser to hide the text until the web font is fully loaded. This will trigger a flash of invisible text, otherwise called FOIT, like that shown in the example below:

FOIT Flash Of Invisible Text

Here, the browser delays rendering the text until the font is loaded. We’ve added a background color to the page so we know when the page is rendered while waiting to display the text.

Meanwhile, a default value of auto tells the browser to apply its default font rendering. This implementation differs across browsers.

For use cases where “content is king”, we usually prefer FOUT over FOIT so that users can read content as soon as possible.

Now that we know some of the issues with fonts loading, we can improve the loading experience using three strategies:

  1. Eliminate the external network request by serving the fonts locally from our domain
  2. Preloading the font so that browser can schedule downloading it early
  3. Use the CSS size-adjust property to reduce layout shifts when switching between fonts

We can manually implement these optimization strategies, but let’s 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, this implementation not only results in a faster load time, it also ensures we’re not violating the European Union’s General Data Protection Regulation (GDPR) by not transferring the user’s personal data to a third-party system like Google.

That is not the case when we embed the web font into the <head> as we’ve seen earlier.

In addition, the next/font system implements a strategy that prevents layout shift or significantly reduces the impact using the CSS size-adjust property.

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 covered 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({ subsets: ['latin'] });

console.log(dancingScript);

export default function Home() {
  return (
    // ...
  );
}

N.B., if a font name includes a space (for instance, “Dancing Script”), we must replace the space with an underscore (i.e *Dancing_Script*)

In the code above, we assign the font instance to a dancingScript variable and log 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 in the terminal:

Browser Developer Tools Panel Showing Returned Object For Dancing Script Variable

The font loader returns an object containing options that we can use to apply the font style.

For optimization purposes, we subset the font to Latin. This strips out unrequired languages from the web font. Doing this ensures a preload link for the required font language is also added in the <head> element:

Subset Font Latin Strip Out Unrequired Languages

The preload suggests to the browser that it should schedule the font for early download.

If we recall, 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:swap. By combining the display implementation together with preloading the font and then the underlying CSS size-adjust property used by the Next.js font system, we’ll get the most effective result.

Applying font styles

As we saw earlier, 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>
  );
}

Now, we can save the file and restart the dev server.

If we check the frontend, we should see the font applied. Even if the font request is slower than expected, we will not experience a layout shift or a FOUT.

The browser developer tools shows that the request is within our infrastructure and not from a delivery service:

Browser Developer Tools Panel With Red Arrow Pointing To Updated Request Url Showing Request Now Comes From Within Infrastructure

Reusing fonts

If we want to use the “Dancing Script” font again in another file, say pages/about.js, we could call the font function in that file and use it as we did before. However, loading the same font function in multiple files is not advisable because:

  1. Updating the font becomes rigid; we would have to manually change the font in separate files instead of a single place
  2. Multiple instances of the font are hosted

In this scenario, the recommendation is to create a separate file where we load the font. Then, we can import the font in each file that requires it.

So, let’s create a utils/fonts.js file and export the “Dancing Script” font as a constant:

import { Dancing_Script } from 'next/font/google';
export const dancingScript = Dancing_Script({ subsets: ['latin'] });

Save the file.

Now, we can reference the font from the pages/index.js and pages/about.js files:

import { dancingScript } from '../utils/fonts';

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.

In the utils/fonts.js file, let’s also import an Oswald variable font from Google and export it so we can use it in another file:

import { Dancing_Script, Oswald } from 'next/font/google';

export const dancingScript = Dancing_Script({ subsets: ['latin'] });
export const oswald = Oswald({ subsets: ['latin'] });

Save the file and import the font in the pages/index.js file so we have the following:

import { dancingScript, oswald } from '../utils/fonts';

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 applied the Oswald variable font to the wrapper element to make it available for the child elements. The paragraph within the wrapper will inherit the font. Meanwhile, the font on theelement will override the wrapper font.

See the output below:

Browser Open To Localhost With Title Text In Dancing Script Font Reading Home Page Above Paragraph Text In Oswald Font. Developer Tools Panel Is Open With Red Arrow Pointing To Overridden Wrapper Font In H1

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.

Adding 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 open the utils/fonts.js file and import it from next/font/google so that we can export an instance of it from the file:

import {
  Dancing_Script,
  Oswald,
  Merriweather,
} from 'next/font/google';

// ...

export const merriweather = Merriweather({
  weight: ['300', '400', '700'],
  style: ['normal', 'italic'],
  subsets: ['latin'],
});

We added a required weight option and some optional style values in the font instance. If we save the file, we can now apply the generated class to a document element. Let’s do that in the pages/index.js file:

import {
  // ...
  merriweather
} from '../utils/fonts';

export default function Home() {
  return (
   // ...
      <p className={merriweather.className}>
        This text uses a non variable font
      </p>
    // ...
    </div>
  );
}

If we save the file, 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);
}

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

Browser Developer Tools Panel Showing Returned Object For Antonio Font Variable

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 Componentwrapper should look like this:

<div className={antonio.variable}>
  <Component {...pageProps} />
</div>

From here, we can use the font with Tailwind CSS — as you will see later in this article — 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. 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 SquirrelWebfont Generator. Then, we will add the bold and normal weight variations of the font to our project’s [public/fonts](https://github.com/Ibaslogic/Next.js-fonts-optimization/tree/main/public/fonts) folder:

Project File Structure With Public Folder Open To Show Fonts Subfolder, Which Is Also Open And Contains Two Cooper Hewitt Font Variations

Using locally configured fonts

Similar to using Google Fonts with Next.js, we can load the local fonts across the entire document or use them 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';

Next, 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 the method used for Google Fonts that we covered 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:

.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 have already 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:

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.

Configuring tailwind.config.js file

Let’s 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 this:

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;

If we want to apply one of the utilities (for instance, font-cooper) globally to the document, we can add it as className to the Component wrapper element in the pages/_app.js file:

function MyApp({ Component, pageProps }) {
  return (
    <>
      {/* ... */}
      <div className={`${antonio.variable} font-cooper`}>
        <Component {...pageProps} />
      </div>
    </>
  );
}

export default MyApp;

Conclusion

As we have seen in this article, 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 article. If you have questions or contributions, share your thoughts in the comment section. See the complete source code for the project on GitHub.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

One Reply to “Next.js font optimization: Adding custom and Google fonts”

  1. LogRocket always have the best articles – thanks for the article, really well written

Leave a Reply