How to optimize custom fonts with next/font

Next.js 13 introduced the next/font package which allows for optimized self-hosting for any font file. This means you can serve your own fonts or fonts from Google Fonts, use them with CSS variables and achieve zero layout shift.

Lars smiling

Published β€” Updated β€” 6 min read

You can now migrate to the built-in next/font by running
npx @next/codemod@latest built-in-next-font . πŸŽ‰
The @next/font package will be removed in Next.js 14.

Of the many exciting features announced in Next.js 13, the newΒ @next/fontΒ module is a humble yet powerful addition that significantly simplifies font optimization.

next/font automatically optimizes your fonts and removes external network requests for improved privacy and performance. In addition, it generates fallback fonts with best-practices values for font-display, size-adjust (and more) to achieve zero layout shift as your app loads.

Zero layout shift is important as it ensures that your app is visually stable, which is a web vitals metric (CLS) relevant to how users perceive performance on the web. In short, web pages with less shifting and interference in the layout as the page loads are perceived as more performant.

How to install @next/font

In Next.js 13.2 and later, next/font is built-in to Next.js and no longer needs to be installed.

Add the package as a dependency to begin:

yarn add @next/font
# or
npm i @next/font
# or
pnpm i @next/font

Goal: self-host multiple font families and use them globally with CSS variables

Defining CSS variables on :root or html allows you to use that variable anywhere in your app. This is particularly useful when defining app-wide styling like fonts and colours.

Take this contrived example:

index.html
<html>
  <head>
    <style>
      :root {
        --font-base: 'myFont';
      }
 
      body {
        font-family: var(--font-base);
      }
    </style>
  </head>
  <body>
    <h1>G'day cobba 🦘</h1>
  </body>
</html>

With this approach, you can easily change your app's font by only changing one line of CSS. Additionally, your CSS is now far more robust as there's only one source of truth that defines exactly what --font-base actually is.

When using localFont from next/font/local you can load multiple font-families or font weights by passing in an array of items to src.

const myFont = localFont({
  src: [
    {
      path: '../assets/fonts/my-font-regular.woff2',
      weight: '300',
    },
    {
      path: '../assets/fonts/my-font-bold.woff2',
      weight: '600',
    },
  ],
})

However, the approach in the Next.js docs doesn't cater to cases where you want to use the CSS variable on body. In addition, using next/font in _document.tsx where the body tag is rendered is also not supported.

error - Font loader error:
Font loaders cannot be used within pages/_document.js.

Working around this constraint is luckily straightforward by using <style jsx global> in _app, so let's put it all together:

pages/_app.tsx
import 'src/styles/globals.scss'
import localFont from 'next/font/local'
 
import type { AppProps } from 'next/app'
 
const myFont = localFont({
  src: [
    {
      path: '../assets/fonts/my-font-regular.woff2',
      weight: '300',
    },
    {
      path: '../assets/fonts/my-font-bold.woff2',
      weight: '600',
    },
  ],
  fallback: ['Helvetica', 'ui-sans-serif'],
})
 
function App({ Component, pageProps }: AppProps): JSX.Element {
  return (
    <>
      <style jsx global>{`
        :root {
          --font-base: ${myFont.style.fontFamily};
        }
      `}</style>
      <Component {...pageProps} />
    </>
  )
}
 
export default App

Your font files can be colocated inside of pages. But, putting them alongside your assets causes a bit less clutter. To make things a bit clearer, this is the folder structure used for the solution above:

src
β”œβ”€ assets
β”‚  β”œβ”€ fonts
β”‚  β”‚  β”œβ”€ my-font-regular.woff2
β”‚  β”‚  β”œβ”€ my-font-bold.woff2
β”œβ”€ pages
β”‚  β”œβ”€ _app.tsx

After this, you should use the CSS variable --font-base in any global or module-specific stylesheets:

globals.scss
body {
  font-family: var(--font-base);
}

If your Next dev server was running during all of this setup you may get the error below. As stated, just restart your dev server to resolve it.

Error: @next/font/local failed to run or is incorrectly configured.
If you just installed `@next/font`, please try restarting `next dev` and resaving your file.

If all goes to plan and your dev server is up and running again, you should see your fonts being applied to your UI πŸŽ‰

Verifying that your fonts load and optimizations work

To verify that everything works the way it should, you can inspect the output HTML with your browser devtools.

  • Inspect the network tab to check that the font files load
  • Check that the CSS variable is set to root, and that it references a fallback font
  • Check that @font-face declarations are set with matching names

You should see that the correct CSS variable is set in <style> tags added to <head>.

<style>
  :root {
    --font-base: '__fontBase', Helvetica, ui-sans-serif,
      '__fontBase_Fallback';
  }
</style>

Also added to <head> you should see multiple @font-face declarations referencing the same name added to --font-base, and the files loaded in the network tab.

The last @font-face declaration is special as it declares a fallback font to enable zero layout shift with size-adjust while users wait for your custom font to load.

<style>
  @font-face {
    font-family: '__fontBase';
    src: url(/_next/static/media/88i0c441c.p.woff2) format('woff2');
    font-display: optional;
    font-weight: 300;
  }
 
  @font-face {
    font-family: '__fontBase';
    src: url(/_next/static/media/63a0b432c.p.woff2) format('woff2');
    font-display: optional;
    font-weight: 600;
  }
 
  @font-face {
    font-family: '__fontBase_Fallback';
    src: local('Arial');
    ascent-override: 98.02%;
    descent-override: 26.35%;
    line-gap-override: 0%;
    size-adjust: 101.72%;
  }
</style>

These little tweaks will make your CSS cleaner and more flexible, and your app will load faster with little to no layout shift. Ultimately giving your users a more enjoyable experience.

References

Go to posts