21

I'm using global style from styled components with next.js and every time I reload my page I can see the font flickering.

font flash

I have my font files in public/fonts/Inconsolata

I've looked everywhere in spectrum chat, next.js github issues but can't seem to find any solution.

pages/index.js

import styled from 'styled-components';

const H1 = styled.h1`
  font-family: 'Inconsolata Bold';
  font-weight: 700;
  color: #000;
`;

const index = () => {
  return (
    <div>
      <H1>font flashes</H1>
    </div>
  );
};

export default index;

pages/_app.js

import App from 'next/app';
import React from 'react';

import GlobalStyle from '../src/style/globalStyle';

export default class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props;
    return (
      <>
        <GlobalStyle />
        <Component {...pageProps} />
      </>
    );
  }
}

pages/_document.js

import Document from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: App => props => sheet.collectStyles(<App {...props} />)
        });

      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        )
      };
    } finally {
      sheet.seal();
    }
  }
}

style/globalStyle.js

import { createGlobalStyle } from 'styled-components';

const globalStyle = createGlobalStyle`
  @font-face {
    font-family: 'Inconsolata';
    src:  url('./fonts/Inconsolata/Inconsolata-Regular.woff2') format('woff2'),
          url('./fonts/Inconsolata/Inconsolata-Regular.woff') format('woff');
    font-weight: 400;
    font-style: normal;
    font-display: fallback;
  }
  @font-face {
    font-family: 'Inconsolata Bold';
    src:  url('./fonts/Inconsolata/Inconsolata-Bold.woff2') format('woff2'),
          url('./fonts/Inconsolata/Inconsolata-Bold.woff') format('woff');
    font-weight: 700;
    font-style: bold;
    font-display: fallback;
  }
`;

export default globalStyle;
m00
  • 297
  • 1
  • 5
  • 13

5 Answers5

9

UPDATE:

Next.js released a new feature called Automatic Webfont Optimization.

Just include your font (it works only with Google Fonts so far) and it will be included as raw css on build-time.

// Before
<link
  href="https://fonts.googleapis.com/css2?family=Inter"
  rel="stylesheet"
/>

// After
<style data-href="https://fonts.googleapis.com/css2?family=Inter">
  @font-face{font-family:'Inter';font-style:normal.....
</style>

Check out how Next.js guys handle it on their website, which is open-source and can be found here. Check it out, it worked for me.

  1. Import your used font in css @font-face via preload link

    <link
        rel="preload"
        href="/assets/my-font.woff2"
        as="font"
        type="font/woff2"
    />
    
  2. Your font declaration should be on SSR HTML page, so use <style jsx global /> to include it in your page. It can be an external file or right directly in style element.

vxn
  • 143
  • 1
  • 8
6

I had a similar problem with NextJS 12, Google Fonts and SCSS modules.

My partial solution was to

  1. 'preload' the resources being requested in any @font-face rules in any CSS file - load the fonts more eagerly
  2. set font-display: optional - tell CSS to use fallback if font not loaded in time

This means no Flash Of Unstyled Text (FOUT) but on slower connections a fallback font will be used on first load instead.

   <head>
      <link rel="preconnect" href="https://fonts.googleapis.com" />
      <link
        rel="preload"
        href="https://fonts.gstatic.com/s/inconsolata/v21/QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyya15IDhunA.woff2"
        as="font"
        type="font/woff2"
        crossOrigin=""
      />
      <link
        rel="preload"
        href="https://fonts.gstatic.com/s/inconsolata/v21/QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyx615IDhunJ_o.woff2"
        as="font"
        type="font/woff2"
        crossOrigin=""
      />
       // CSS file with @font-face rules and font-display: optional
      <link
        href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@300;400;700&display=optional"
        rel="stylesheet"
      />
</head>

In this example, the last link element requests a CSS file which has a number of @font-face rules with src declarations and url() values.

If the resources in the url() functions aren't preloaded in the head then they wont be requested until the CSS is parsed.

I think this is what causes the FOUT.

By including <link rel="preload" src="..."/> elements fonts can be loaded sooner.

And setting font-display: optional tells the browser that if the font hasn't loaded in time to use a fallback.

The site I was working on: Soundboard

jackhkm
  • 71
  • 1
  • 5
  • Thank you! This was the _only_ setup that worked for me after lots of searching! – Sam Sverko Dec 29 '21 at 20:20
  • As you can see, the problem with that approach is that the font's version changes. As you can see in the file: https://fonts.googleapis.com/css2?family=Inconsolata:wght@300;400;700&display=optional The "preload" link is pointing at v21 and the file is already at v31. – iwaduarte Apr 23 '23 at 20:28
2

Create an style.css file in your public/fonts/ directory. Copy and paste your @font-face part in style.css.

style.css

@font-face {
  font-family: "Inconsolata";
  src: url("./fonts/Inconsolata/Inconsolata-Regular.woff2") format("woff2"),
    url("./fonts/Inconsolata/Inconsolata-Regular.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: fallback;
}

@font-face {
  font-family: "Inconsolata Bold";
  src: url("./fonts/Inconsolata/Inconsolata-Bold.woff2") format("woff2"),
    url("./fonts/Inconsolata/Inconsolata-Bold.woff") format("woff");
  font-weight: 700;
  font-style: bold;
  font-display: fallback;
}

In your pages/_app.js import the style.css file in head.

_app.js

import App from 'next/app';
import React from 'react';
import Head from "next/head";

import GlobalStyle from '../src/style/globalStyle';

export default class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props;
    return (
      <>
        <GlobalStyle />
        <Head>
          <link rel="stylesheet" href="/fonts/style.css" />
        </Head>
        <Component {...pageProps} />
      </>
    );
  }
}
0

Hat tip to Raul Sanchez on dev.to for the answer to this one:

Next doesn't fetch styled-components styles on the server, to do that you need to add this page to pages/_document.js:

import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        })

      const initialProps = await Document.getInitialProps(ctx)
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      }
    } finally {
      sheet.seal()
    }
  }
}

This code may update, so check Next's styled-components example for the latest.

fredrivett
  • 5,419
  • 3
  • 35
  • 48
-1

I had the same problem and after hours of experimenting with different methods npm package fontfaceobserver solved the problem for me.

With the package you can tell your app to render only after fonts are loaded, thus avoiding FOUT like so:

import FontFaceObserver from "fontfaceobserver";

const font = new FontFaceObserver("Inconsolata");

font.load().then(()=> {
  ReactDOM.render(<App />,document.getElementById("root"));
}
Smlok
  • 598
  • 6
  • 19
  • I don't think you really want to have your whole app be waiting to render based on the font. Also, this wouldn't work for SPA, and it's odd to do for SSR. – AndrewSteinheiser Apr 29 '22 at 09:15