Search code examples
next.jsfontstailwind-cssstorybook

Storybook Question: Local fonts not loading - Next 13 + Tailwind + Storybook 7


Initial Question:

My setup all works as it should, but I have one problem: my custom fonts don't load in Storybook.

(My main question is about the missing CSS variable, go to the bottom to see it. I'm just posting all the code for reference.)

Part of the directory structure:

Screenshot 2023-09-02 at 11 29 26

Here is where I am fixing the local font with Next, in "app/(site)/layout.tsx":

layout.tsx

import "../styles/globals.css"
import type { Metadata } from "next"
import localFont from "@next/font/local"
import Header from "@/app/components/Header"
import Footer from "@/app/components/Footer"
import { getCachedClient } from "@/sanity/lib/getClient"
import SiteConfigQuery from "@/sanity/queries/site-config/siteConfigQuery"
import { MobileDrawer } from "../components/MobileDrawer"

const tTFirs = localFont({
    src: [
        {
            path: "../../public/fonts/TypeType - TT Firs Regular.otf",
            weight: "300"
        },
        {
            path: "../../public/fonts/TypeType - TT Firs Medium.otf",
            weight: "400"
        },
        {
            path: "../../public/fonts/TypeType - TT Firs Medium Italic.otf",
            weight: "400",
            style: "italic"
        },
        {
            path: "../../public/fonts/TypeType - TT Firs Italic.otf",
            weight: "400",
            style: "italic"
        },
        {
            path: "../../public/fonts/TypeType - TT Firs Bold.otf",
            weight: "700"
        },
        {
            path: "../../public/fonts/TypeType - TT Firs Bold Italic.otf",
            weight: "700",
            style: "italic"
        }
    ],
    variable: "--font-tt-firs"
})

export const metadata: Metadata = {
    title: "Next sanity starter",
    description: "Generated by create next app"
}

export default async function RootLayout({ children }: { children: React.ReactNode }) {
    const config = await getCachedClient(undefined)(SiteConfigQuery.GET_BY_ID)

    return (
        <html lang="en">
            <body className={`font-sans ${tTFirs.variable}`}>
                {
                    <main className="bg-black">
                        <div className="container mx-auto">
                            <Header config={config[0]} />
                            <MobileDrawer config={config[0]} />
                            {children}
                            <Footer config={config[0]} />
                        </div>
                    </main>
                }
            </body>
        </html>
    )
}

.storybook/main.ts

import type { StorybookConfig } from "@storybook/nextjs"

const config: StorybookConfig = {
    stories: ["../app/**/*.mdx", "../app/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
    addons: [
        "@storybook/addon-links",
        "@storybook/addon-essentials",
        "@storybook/addon-onboarding",
        "@storybook/addon-interactions",
        {
            name: "@storybook/addon-styling",
            options: {
                // Check out https://github.com/storybookjs/addon-styling/blob/main/docs/api.md
                // For more details on this addon's options.
                postCss: {
                    implementation: require.resolve("postcss")
                }
            }
        }
    ],
    framework: {
        name: "@storybook/nextjs",
        options: {}
    },
    docs: {
        autodocs: "tag"
    },
    staticDirs: [
        {
            from: "../public/fonts",
            to: "public/fonts"
        },
        "../public"
    ]
}
export default config

.storybook/preview.ts

import type { Preview } from "@storybook/react"
import "../app/styles/globals.css"

const preview: Preview = {
    parameters: {
        actions: { argTypesRegex: "^on[A-Z].*" },
        controls: {
            matchers: {
                color: /(background|color)$/i,
                date: /Date$/
            }
        }
    }
}

export default preview

tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
    darkMode: ["class"],
    content: [
        "./pages/**/*.{js,ts,jsx,tsx,mdx}",
        "./components/**/*.{js,ts,jsx,tsx,mdx}",
        "./app/**/*.{js,ts,jsx,tsx,mdx}",
        "./src/**/*.{js,ts,jsx,tsx,mdx}"
    ],
    theme: {
        container: {
            center: true,
            padding: "0rem",
            screens: {
                "3xl": "2200px"
            }
        },
        extend: {
            fontFamily: {
                sans: ["var(--font-tt-firs)"]
            },
            screens: {
                xs: "460px",
                "3xl": "2200px"
            },
            ...
        }
    },
    plugins: [require("tailwindcss-animate")]
}

app/styles/globals.css

@tailwind base;
@tailwind components;
@tailwind utilities;

@import url(./projectsGrid.css);
@import url(./morphingBackground.css);
@import url(./mobileDrawer.css);
@import url(./afters.css);

:root {
    --morphing-bg-height: 65.625rem;
}

@layer base {
    :root {
        --background: 0 0% 100%;
        --foreground: 0 0% 3.9%;

        --card: 0 0% 100%;
        --card-foreground: 0 0% 3.9%;

        --popover: 0 0% 100%;
        --popover-foreground: 0 0% 3.9%;

        --primary: 0 0% 9%;
        --primary-foreground: 0 0% 98%;

        --secondary: 0 0% 96.1%;
        --secondary-foreground: 0 0% 9%;

        --muted: 0 0% 96.1%;
        --muted-foreground: 0 0% 45.1%;

        --accent: 0 0% 96.1%;
        --accent-foreground: 0 0% 9%;

        --destructive: 0 84.2% 60.2%;
        --destructive-foreground: 0 0% 98%;

        --border: 0 0% 89.8%;
        --input: 0 0% 89.8%;
        --ring: 0 0% 3.9%;

        --radius: 0.5rem;

        --size-xs: 460px;
        --size-sm: 640px;
        --size-md: 768px;
        --size-lg: 1024px;
        --size-xl: 1280px;
        --size-2xl: 1536px;
        --size-3xl: 2200px;

        --nav-item-width: 7.625rem;
    }

    .dark {
        --background: 0 0% 3.9%;
        --foreground: 0 0% 98%;

        --card: 0 0% 3.9%;
        --card-foreground: 0 0% 98%;

        --popover: 0 0% 3.9%;
        --popover-foreground: 0 0% 98%;

        --primary: 0 0% 98%;
        --primary-foreground: 0 0% 9%;

        --secondary: 0 0% 14.9%;
        --secondary-foreground: 0 0% 98%;

        --muted: 0 0% 14.9%;
        --muted-foreground: 0 0% 63.9%;

        --accent: 0 0% 14.9%;
        --accent-foreground: 0 0% 98%;

        --destructive: 0 62.8% 30.6%;
        --destructive-foreground: 0 0% 98%;

        --border: 0 0% 14.9%;
        --input: 0 0% 14.9%;
        --ring: 0 0% 83.1%;
    }
}

@layer base {
    * {
        @apply border-border;
    }

    body {
        @apply bg-background text-foreground;
    }

    @layer base {
        h1 {
            @apply ls-h1;
        }

        h2 {
            @apply ls-h2;
        }

        h3 {
            @apply ls-h3;
        }

        h4 {
            @apply ls-h4;
        }
    }
}

@layer components {
    .ls-h1 {
        color: white;
        text-align: center;
        font-size: 4rem;
        font-style: normal;
        font-weight: bold;
        line-height: 105%;
        letter-spacing: -0.12rem;
    }

    .ls-h2 {
        color: white;
        font-family: helvetica;
        text-align: center;
        font-size: 1rem;
        font-style: normal;
        font-weight: 400;
        line-height: 125%;
        letter-spacing: -0.01375rem;
    }

    .ls-h3 {
        @apply text-base;
    }

    .ls-h4 {
        @apply text-base;
    }
}

@layer utilities {
    .flex-center {
        display: flex;
        justify-content: center;
        align-items: center;
    }
}

html {
    scroll-behavior: smooth;
}

body.lock-scroll {
    overflow: hidden;
}

body.lock-scroll #home {
    pointer-events: none;
}

So, we can see in the layout.tsx file, I am doing this: <body className={`font-sans ${tTFirs.variable}`}>

This means I am attaching the variable to the body.

Screenshot 2023-09-02 at 11 34 47

So then when I am inspecting the font in the dev tools, I can see the variable is not there at all. (It is there as it should in my normal dev environment, so no problems there.)

I've tried a bunch of different combination with the staticDirs setting as well, as discussed here: storybook/nextjs font local link

Update, 1 Day Later

So I had a bit of progress:

import * as React from "react"
import type { Preview } from "@storybook/react"
import "../app/styles/globals.css"
import { tTFirs } from "../app/lib/fonts"

const preview: Preview = {
    parameters: {
        actions: { argTypesRegex: "^on[A-Z].*" },
        controls: {
            matchers: {
                color: /(background|color)$/i,
                date: /Date$/
            }
        }
    },
    decorators: [
        Story => (
            <div className={`${`font-sans ${tTFirs.variable}`}`}>
                <Story />
            </div>
        )
    ]
}

export default preview

I added this decorator in preview.tsx (formerly preview.ts), and now the CSS variable is at least recognized. Also the fonts are in the stylesheet, as we can see in this picture:

enter image description here enter image description here

But the images are still not loading. (In my normal environment, no problem. The fonts are loading from "__next/static/media). For simplicity I put all my fonts into my public folder, like this:

enter image description here

I an image in there as well, just to check that my staticDirs: ["../public"] was indeed working. And that image is loading up fine.

We can see here that the image is given to me by Storybook:

enter image description here

But no fonts, as far as I can see. The text just has the fallback font.


Solution

  • I finally managed to fix it.

    In main.ts, I now have this as my staticDirs value:

    import type { StorybookConfig } from "@storybook/nextjs"
    
    const config: StorybookConfig = {
        stories: ["../app/**/*.mdx", "../app/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
        addons: [
            "@storybook/addon-links",
            "@storybook/addon-essentials",
            "@storybook/addon-onboarding",
            "@storybook/addon-interactions",
            {
                name: "@storybook/addon-styling",
                options: {
                    // Check out https://github.com/storybookjs/addon-styling/blob/main/docs/api.md
                    // For more details on this addon's options.
                    postCss: {
                        implementation: require.resolve("postcss")
                    }
                }
            }
        ],
        framework: {
            name: "@storybook/nextjs",
            options: {}
        },
        docs: {
            autodocs: "tag"
        },
        staticDirs: ["../public", { from: "../public/fonts", to: "/fonts" }]
    }
    export default config
    

    This is my file structure, with public/font:

    enter image description here

    Now, I am importing the local fonts for my main environment (in Next’s root Layout file) like this:

    import "../styles/globals.css"
    import type { Metadata } from "next"
    import Header from "@/app/components/Header"
    import Footer from "@/app/components/Footer"
    import { getCachedClient } from "@/sanity/lib/getClient"
    import SiteConfigQuery from "@/sanity/queries/site-config/siteConfigQuery"
    import { MobileDrawer } from "../components/MobileDrawer"
    import { tTFirs } from "../lib/fonts"
    
    export const metadata: Metadata = {
        title: "Next sanity starter",
        description: "Generated by create next app"
    }
    
    export default async function RootLayout({ children }: { children: React.ReactNode }) {
        const config = await getCachedClient(undefined)(SiteConfigQuery.GET_BY_ID)
    
        return (
            <html lang="en">
                <body className={`font-sans ${tTFirs.variable}`}>
                    {
                        <main className="bg-black">
                            <div className="container mx-auto">
                                <Header config={config[0]} />
                                <MobileDrawer config={config[0]} />
                                {children}
                                <Footer config={config[0]} />
                            </div>
                        </main>
                    }
                </body>
            </html>
        )
    }
    

    In lib/fonts I have my localfont definition from Next:

    import localFont from "next/font/local"
    
    export const tTFirs = localFont({
        src: [
            {
                path: "../../public/fonts/TT-Firs-Regular.otf",
                weight: "300"
            },
            {
                path: "../../public/fonts/TT-Firs-Medium.otf",
                weight: "400"
            },
            {
                path: "../../public/fonts/TT-Firs-Medium-Italic.otf",
                weight: "400",
                style: "italic"
            },
            {
                path: "../../public/fonts/TT-Firs-Italic.otf",
                weight: "400",
                style: "italic"
            },
            {
                path: "../../public/fonts/TT-Firs-Bold.otf",
                weight: "700"
            },
            {
                path: "../../public/fonts/TT-Firs-Bold-Italic.otf",
                weight: "700",
                style: "italic"
            }
        ],
        variable: "--font-tt-firs"
    })
    
    

    This has always worked fine.

    But then this is what I did in .storybook/preview.tsx:

    import * as React from "react"
    import type { Preview } from "@storybook/react"
    import localFont from "next/font/local"
    import "../app/styles/globals.css"
    
    export const tTFirs = localFont({
        src: [
            {
                path: "../fonts/TT-Firs-Regular.otf",
                weight: "300"
            },
            {
                path: "../fonts/TT-Firs-Medium.otf",
                weight: "400"
            },
            {
                path: "../fonts/TT-Firs-Medium-Italic.otf.otf",
                weight: "400",
                style: "italic"
            },
            {
                path: "../fonts/TT-Firs-Italic.otf",
                weight: "400",
                style: "italic"
            },
            {
                path: "../fonts/TT-Firs-Bold.otf",
                weight: "700"
            },
            {
                path: "../fonts/TT-Firs-Bold-Italic.otf",
                weight: "700",
                style: "italic"
            }
        ],
        variable: "--font-tt-firs"
    })
    
    const preview: Preview = {
        parameters: {
            actions: { argTypesRegex: "^on[A-Z].*" },
            controls: {
                matchers: {
                    color: /(background|color)$/i,
                    date: /Date$/
                }
            }
        },
        decorators: [
            Story => (
                <div className={`${`font-sans ${tTFirs.variable}`}`}>
                    <Story />
                </div>
            )
        ]
    }
    
    export default preview
    

    Notice the "../fonts" in there in the path to the font, which is mapping to the "{ from: "../public/fonts", to: "/fonts" }" in staticDirs in .storybook/main.ts.

    The reason it is “../fonts” is because this will be run in Storybook’s runtime environment and the fonts (because of what we specified in staticDirs) will be located at “./fonts”.

    (There is nothing magical about the name “fonts”. I tried to change it to /x in both main.ts and preview.tsx and it still worked.)

    Update: However, I noticed if you have staticDirs: ["../public"] without the mapping, Storybook will create a /fonts directory and put the fonts in there by themselves. So now I actually removed the mapping, and kept the import path like this:

    {
      path: "../fonts/TT-Firs-Medium-Italic.otf",
      weight: "400",
      style: "italic"
    }
    

    and it still worked.

    The main issue

    Now here was the main thing that caused this to take so damn long. It turned out that the filename of the font files caused everything to fail; something about blankspace in the file name.

    I found this out after I tried to put another file into the static folder and import it, and it suddenly worked! I realized that this file didn't have any blankspace, so I tried to change the name of all my file and BAAM! It worked!

    Notice the difference in the file names here:

    Before:

    enter image description here

    After:

    enter image description here

    Since this didn’t happen with Next (I could load the font just fine), it might be that Storybook’s static loader or their Webpack config or something has some effect on how the file name needs to be structured in order for it to work. I’m not sure and I don’t feel like going through their source code right now; I just want to make some Stories with the right font.