Search code examples
javascriptcssnext.jsfontstailwind-css

next/font works everywhere except one specific component


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

Inter and App Takeoff typefaces working correctly

modal.tsx via index.tsx

Inter and App Takeoff typefaces not working

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.


Solution

  • 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.