最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - nextfont works everywhere except one specific component - Stack Overflow

programmeradmin0浏览0评论

next/font

Uses Next.js with TypeScript and Tailwind CSS

This is my first time using the new next/font package. I followed Next.js' tutorial, and it was easy to set up. I'm using both Inter and a custom local typeface called App Takeoff. To actually use both of these typefaces, I'm using Tailwind CSS, where Inter is connected to font-sans and App Takeoff is connected to font-display.

Everything works except in one spot

I have done plenty of testing between files, and for some reason both typefaces work everywhere except my Modal component. (See Helpful Update at the bottom for why it doesn't work in the Modal component.)

Example

index.tsx

modal.tsx via index.tsx

As you can see, the typefaces work just fine when they aren't inside the modal, but as soon as they're in the modal they don't work.

Here's some relevant code:

// app.tsx

import '@/styles/globals.css'
import type { AppProps } from 'next/app'

import { Inter } from 'next/font/google'
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter'
})

import localFont from 'next/font/local'
const appTakeoff = localFont({
  src: [
    {
      path: '../fonts/app-takeoff/regular.otf',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.eot',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.woff2',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.woff',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.ttf',
      weight: '400',
      style: 'normal'
    }
  ],
  variable: '--font-app-takeoff'
})

const App = ({ Component, pageProps }: AppProps) => {
  return (
    <div className={`${inter.variable} font-sans ${appTakeoff.variable}`}>
      <Component {...pageProps} />
    </div>
  )
}

export default App
// modal.tsx

import type { FunctionComponent } from 'react'
import type { Modal as ModalProps } from '@/typings/components'
import React, { useState } from 'react'
import { Fragment } from 'react'
import { Transition, Dialog } from '@headlessui/react'

const Modal: FunctionComponent<ModalProps> = ({ trigger, place = 'bottom', className, addClass, children }) => {

  const [isOpen, setIsOpen] = useState(false),
        openModal = () => setIsOpen(true),
        closeModal = () => setIsOpen(false)

  const Trigger = () => React.cloneElement(trigger, { onClick: openModal })

  const enterFrom = place === 'center'
    ? '-translate-y-[calc(50%-12rem)]'
    : 'translate-y-full sm:-translate-y-[calc(50%-12rem)]'

  const mainPosition = place === 'center'
    ? '-translate-y-1/2'
    : 'translate-y-0 sm:-translate-y-1/2'

  const leaveTo = place === 'center'
    ? '-translate-y-[calc(50%+8rem)]'
    : 'translate-y-full sm:-translate-y-[calc(50%+8rem)]'

  return (
    <>
    
      <Trigger />

      <Dialog open={isOpen} onClose={closeModal} className='z-50'>

        {/* Backdrop */}
        <div className='fixed inset-0 bg-zinc-200/50 dark:bg-zinc-900/50 backdrop-blur-sm cursor-pointer' aria-hidden='true' />

        <Dialog.Panel
          className={`
            ${className || `
              fixed left-1/2
              ${
                place === 'center'
                ? 'top-1/2 rounded-2xl'
                : 'bottom-0 sm:bottom-auto sm:top-1/2 rounded-t-2xl xs:rounded-b-2xl'
              }
              bg-zinc-50 dark:bg-zinc-900
              w-min
              -translate-x-1/2
              overflow-hidden
              px-2 xs:px-6
              shadow-3xl shadow-primary-400/10
            `}
            ${addClass || ''}
          `}
        >
          {children}
              
        </Dialog.Panel>

        <button
          onClick={closeModal}
          className='
            fixed top-4 right-4
            bg-primary-600 hover:bg-primary-400
            rounded-full
            h-7 w-7 desktop:hover:w-20
            overflow-x-hidden
            transition-[background-color_width] duration-300 ease-in-out
            group/button
          '
          aria-role='button'
        >
          Close
        </button>

      </Dialog>

    </>
  )
}

export default Modal

I hope this information helps. Let me know if there's anything else that would be helpful to know.

Helpful Update

Thank you Jonathan Wieben for explanation of why this isn't working (See Explanation). The issue simply has to do with the scope of the applied styles, and Headless UI's usage of the React Portal component. If anyone has some ideas of how I can either change where the Portal is rendered or change the scope of the styles, that would be super helpful. Jonathan Wieben pointed out a way to do this, however—from my testing—it doesn't work with Tailwind CSS.

next/font

Uses Next.js with TypeScript and Tailwind CSS

This is my first time using the new next/font package. I followed Next.js' tutorial, and it was easy to set up. I'm using both Inter and a custom local typeface called App Takeoff. To actually use both of these typefaces, I'm using Tailwind CSS, where Inter is connected to font-sans and App Takeoff is connected to font-display.

Everything works except in one spot

I have done plenty of testing between files, and for some reason both typefaces work everywhere except my Modal component. (See Helpful Update at the bottom for why it doesn't work in the Modal component.)

Example

index.tsx

modal.tsx via index.tsx

As you can see, the typefaces work just fine when they aren't inside the modal, but as soon as they're in the modal they don't work.

Here's some relevant code:

// app.tsx

import '@/styles/globals.css'
import type { AppProps } from 'next/app'

import { Inter } from 'next/font/google'
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter'
})

import localFont from 'next/font/local'
const appTakeoff = localFont({
  src: [
    {
      path: '../fonts/app-takeoff/regular.otf',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.eot',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.woff2',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.woff',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.ttf',
      weight: '400',
      style: 'normal'
    }
  ],
  variable: '--font-app-takeoff'
})

const App = ({ Component, pageProps }: AppProps) => {
  return (
    <div className={`${inter.variable} font-sans ${appTakeoff.variable}`}>
      <Component {...pageProps} />
    </div>
  )
}

export default App
// modal.tsx

import type { FunctionComponent } from 'react'
import type { Modal as ModalProps } from '@/typings/components'
import React, { useState } from 'react'
import { Fragment } from 'react'
import { Transition, Dialog } from '@headlessui/react'

const Modal: FunctionComponent<ModalProps> = ({ trigger, place = 'bottom', className, addClass, children }) => {

  const [isOpen, setIsOpen] = useState(false),
        openModal = () => setIsOpen(true),
        closeModal = () => setIsOpen(false)

  const Trigger = () => React.cloneElement(trigger, { onClick: openModal })

  const enterFrom = place === 'center'
    ? '-translate-y-[calc(50%-12rem)]'
    : 'translate-y-full sm:-translate-y-[calc(50%-12rem)]'

  const mainPosition = place === 'center'
    ? '-translate-y-1/2'
    : 'translate-y-0 sm:-translate-y-1/2'

  const leaveTo = place === 'center'
    ? '-translate-y-[calc(50%+8rem)]'
    : 'translate-y-full sm:-translate-y-[calc(50%+8rem)]'

  return (
    <>
    
      <Trigger />

      <Dialog open={isOpen} onClose={closeModal} className='z-50'>

        {/* Backdrop */}
        <div className='fixed inset-0 bg-zinc-200/50 dark:bg-zinc-900/50 backdrop-blur-sm cursor-pointer' aria-hidden='true' />

        <Dialog.Panel
          className={`
            ${className || `
              fixed left-1/2
              ${
                place === 'center'
                ? 'top-1/2 rounded-2xl'
                : 'bottom-0 sm:bottom-auto sm:top-1/2 rounded-t-2xl xs:rounded-b-2xl'
              }
              bg-zinc-50 dark:bg-zinc-900
              w-min
              -translate-x-1/2
              overflow-hidden
              px-2 xs:px-6
              shadow-3xl shadow-primary-400/10
            `}
            ${addClass || ''}
          `}
        >
          {children}
              
        </Dialog.Panel>

        <button
          onClick={closeModal}
          className='
            fixed top-4 right-4
            bg-primary-600 hover:bg-primary-400
            rounded-full
            h-7 w-7 desktop:hover:w-20
            overflow-x-hidden
            transition-[background-color_width] duration-300 ease-in-out
            group/button
          '
          aria-role='button'
        >
          Close
        </button>

      </Dialog>

    </>
  )
}

export default Modal

I hope this information helps. Let me know if there's anything else that would be helpful to know.

Helpful Update

Thank you Jonathan Wieben for explanation of why this isn't working (See Explanation). The issue simply has to do with the scope of the applied styles, and Headless UI's usage of the React Portal component. If anyone has some ideas of how I can either change where the Portal is rendered or change the scope of the styles, that would be super helpful. Jonathan Wieben pointed out a way to do this, however—from my testing—it doesn't work with Tailwind CSS.

Share Improve this question edited Mar 1, 2023 at 16:38 andrilla asked Feb 11, 2023 at 18:10 andrillaandrilla 7211 gold badge9 silver badges25 bronze badges 2
  • tailwindcss.com/docs/guides/nextjs have u add tailwindcss plugin for postcss ? – 小聪聪到此一游 Commented Feb 22, 2023 at 14:27
  • Yep. I use that exact documentation from Tailwind. Is there anything you think I should add to the postcss.config.js file, that would make it work? – andrilla Commented Feb 22, 2023 at 19:02
Add a comment  | 

5 Answers 5

Reset to default 12 +25

The Dialog component you are using renders in a portal (see here).

you typically want to render them as a sibling to the root-most node of your React application. That way you can rely on natural DOM ordering to ensure that their content is rendered on top of your existing application UI.

You can confirm this by inspecting your modal DOM element in your browser and seeing if it is indeed placed outside the div wrapper from your App component (I suspect it is).

If so, this is the explanation for why the modal content does not render with the expected font: It is rendered outside the component that defines the font.

To get around this, you could define your font on a higher level, e.g. in your head like described here: Next docs.

I had the exact same problem with headlessui, tailwind and nextjs. I found the solution that was marked correctly way too complicated for something as simple as modal. What worked for me is to insert the same font into the Modal component:

//Modal.tsx
import { Dialog, Transition } from '@headlessui/react';
import { Rubik } from '@next/font/google';

const rubik = Rubik({
  subsets: ['latin'],
  variable: '--font-rubik',
});

type Props = {
  children: React.ReactNode;
  isOpen: boolean;
  closeModal: any;
};

const Modal = ({ children, isOpen, closeModal }: Props) => {
  return (
  <>
  <Transition ...>
    <Dialog ...>
    ...
        <Dialog.Panel
              className={`${rubik.variable} font-sans ...`}>
              ...
        </Dialog.Panel>
    </Dialog>
  </Transition>
  </>
    );
};
export default Modal;

Worked like a charm.

Finally a Solution

...though not perfect...

This solution works, but it doesn't allow us to take full advantage of the loading next/font. Thankfully, it is an easy solution for now.

Since the issue comes from @headlessui/react rendering the Modal component as a child of the <body> element, we need to apply the next/font-generated CSS variables on the <body> element, rather than the <div> element in the App component as shown in the next/font documentation.

Unfortunately, there's no way to add them in the same way you would with the <div> element. What we need to do, is use a more vanilla JavaScript approach, and apply the classes after the page has loaded using document.querySelector('body') and className.add().

Add Class function (optional)

For my solution, I'm using a custom function called addClass. This may not be necessary, but when I first tried body.classList.add(typefaceClasses), it said the string had incorrect characters.

If you want to use the addClass function, here it is:

/**
 * ### Add Class
 * - Adds the specified classes to the specified elements
 * @param {Element|HTMLElement|HTMLElement[]|NodeList|string|undefined} elements An HTML Element, an array of HTML Elements, a Node List, a string (as a selector for a querySelector)
 * @param {string|string[]} classes A string or an array of classes to add to each element
 */
export const addClass = (elements: Element | HTMLElement | HTMLElement[] | NodeList | string, classes: string | string[]) => {

  const elementsType = elements.constructor.name,
        classesType = classes.constructor.name

  let elementList: HTMLElement[] | undefined,
      classesList: string[] | undefined

  // * Convert elements to array
  // @ts-ignore elementsType verifies type
  if (elementsType === 'String') elementList = Array.from(document.querySelectorAll(elements)) // Selector
  // @ts-ignore elementsType varfies type
  if (elementsType.startsWith('HTML')) elementList = [elements] // One HTML Element
  // @ts-ignore elementsType verifies type
  if (elementsType === 'NodeList') elementList = Array.from(elements) // Multiple HTML Elements
  // @ts-ignore elementsType verifies type
  if (elementsType === 'Array') elementList = elements // Array of Elements

  // * Convert classes to array
  // @ts-ignore classesType verifies type
  if (classesType === 'String' && classes.split(' ')) classesList = classes.split(' ')
  // @ts-ignore classesType verifies type
  if (classesType === 'Array') classesList = classes

  if (elementList && classesList) elementList.forEach((element: HTMLElement) =>
    classesList!.forEach((classItem: string) => {
      if (hasClass(element, classItem)) return
      element.classList.add(classItem)
    })
  )
}

Adding Classes to the Body

As you can see in the following example, we use useEffect

// app.tsx

import '@/styles/globals.css'
import type { AppProps } from 'next/app'

import { Inter } from 'next/font/google'
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter'
})

import localFont from 'next/font/local'
const appTakeoff = localFont({
  src: [
    {
      path: '../fonts/app-takeoff/regular.otf',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.eot',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.woff2',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.woff',
      weight: '400',
      style: 'normal'
    },
    {
      path: '../fonts/app-takeoff/regular.ttf',
      weight: '400',
      style: 'normal'
    }
  ],
  variable: '--font-app-takeoff'
})

import { useEffect, useMemo } from 'react'

import { addClass } from '@/utils/class-management'

const App = ({ Component, pageProps }: AppProps) => {

  // Set an array of the classes (use string with classList.add())
  const typefaceClasses = useMemo(() => [
    inter.variable,
    appTakeoff.variable,
    'font-sans'
  ], [])

  useEffect(() => {
    // First we make sure the window is defined
    if (typeof window) {
      // Get the body element
      const body = document.querySelector('body')
      // If the body element is truthy, we add all of the classes to it
      // Otherwise null
      body ? addClass(body, typefaceClasses) : null
    }
  }, [typefaceClasses])

  return (
    <Component {...pageProps} />
  )
}

export default App

What Next?

Hopefully someone will come along one day and give a better solution, or perhaps the Next.js team will add support for the <body> element by default.

This is not an issue when using the App Directory. Now that it's out of beta, I highly recommend using it.

/********* external libraries ****************/
/********* external libraries ****************/

/********* internal libraries ****************/
import { Noto_Sans_TC } from '@next/font/google';
import CustomFont from '@next/font/local';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import type { ReactElement, ReactNode } from 'react';
import './styles.css';

/********* internal libraries ****************/

export type NextPageWithLayout<P = unknown, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode;
};

type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout;
};

const notoSansTC = Noto_Sans_TC({
  weight: ['300', '400', '700', '900'],
  subsets: ['chinese-traditional'],
  display: 'swap',
});

const chappaFont = CustomFont({
  src: '../public/fonts/chappa-Black.ttf',
  variable: '--font-chappa',
});
const cubic11 = CustomFont({
  src: '../public/fonts/Cubic_11_1.013_R.ttf',
  variable: '--font-cubic11',
});

export default function CustomApp({
  Component,
  pageProps: { session, ...pageProps },
}: AppPropsWithLayout) {
  const getLayout = Component.getLayout ?? ((page) => page);

  return (
    <>
      <style jsx global>{`
        .body {
          font-family: ${notoSansTC.style.fontFamily};
        }

        .font-cubic11 {
          font-family: ${cubic11.style.fontFamily};
        }

        .font-chappa {
          font-family: ${chappaFont.style.fontFamily};
        }
      `}</style>
      <Head>
        <title>Welcome</title>
      </Head>
      <div
        className={`${chappaFont.variable} ${cubic11.variable} ${notoSansTC.className}`}
      >
        {getLayout(<Component {...pageProps} />)}
      </div>
    </>
  );
}

I use Next docs and Next docs

发布评论

评论列表(0)

  1. 暂无评论