Styling, fonts and image optimisation

CSS Modules versus global CSS versus Tailwind, self-hosted fonts with next/font, and sizing images correctly with next/image.

CSS Modules, global CSS and Tailwind

// app/guides/page.tsx
import styles from './guides.module.css'      // class names are scoped to this file

export default function Guides({ open }) {
  return (
    <>
      <h1 className={styles.heading}>Guides</h1>
      <div className={['card', open ? 'card-open' : ''].join(' ')} />
    </>
  )
}
/* app/guides/guides.module.css */
.heading { font-size: 2rem; letter-spacing: -0.02em; }
.card { border: 1px solid var(--border); border-radius: 8px; }
  • Import global CSS once, in the root layout. Anywhere else it leaks across every route and the framework warns you.
  • CSS Modules are built in: the import gives you an object whose keys are the class names you wrote, and the emitted names are scoped.
  • create-next-app wires up Tailwind with a PostCSS plugin and a single import in the global stylesheet; utilities remove the naming step without changing how styles are loaded.
  • A class that depends on data belongs in the JSX. Compute the class string from props rather than mutating the DOM or reaching for a second stylesheet.
  • A component library that ships its own CSS needs that file imported once in the root layout, or the first render arrives unstyled.

Fonts with next/font

// app/layout.tsx
import { Inter } from 'next/font/google'
import localFont from 'next/font/local'

const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-sans' })

const brand = localFont({
  src: './fonts/Brand.woff2',
  variable: '--font-brand',
  display: 'swap'
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.variable + ' ' + brand.variable}>
      <body>{children}</body>
    </html>
  )
}
  • next/font downloads and self-hosts the file at build time: no request to a third-party host, no privacy problem and no render-blocking link.
  • Declare the subsets you actually render. Loading a full family for one script means every visitor downloads glyphs they never display.
  • A variable font is one file covering a weight range, which is why it is preferred over loading four separate static weights.
  • Expose the font through a CSS variable and use that in your stylesheet, so components do not need to know a generated class name.
  • Adding another language means adding its subset, not importing a second family.
💡
Self-hosting with next/font plus matching fallback metrics removes a render-blocking request and the layout shift that comes with it. On most sites this is the cheapest visible performance win available.

Images and third-party scripts

import Image from 'next/image'
import Script from 'next/script'

export default function Hero() {
  return (
    <>
      <Image
        src="/hero.jpg"
        alt="Two people reviewing a chart"
        width={1200}
        height={630}
        priority
        sizes="(max-width: 768px) 100vw, 50vw"
      />
      <Image src="https://images.example.com/logo.png" alt="Partner" width={120} height={40} />
      <Script src="https://example.com/widget.js" strategy="lazyOnload" />
    </>
  )
}
  • next/image needs either width and height, or fill inside a positioned container. Without dimensions it cannot reserve space, so the layout shifts as each image loads.
  • Mark the one hero image priority. Everything else is lazy-loaded by default, which is the behaviour you want off-screen.
  • sizes tells the browser how wide the image will be at each breakpoint so it can request a smaller file; omitting it usually means the largest variant is downloaded.
  • Remote hosts must be listed in images.remotePatterns. That is deliberate: without it the optimiser could be used to proxy arbitrary URLs.
  • The optimiser needs a server. A static export has none, so output: 'export' requires unoptimised images — decide that trade-off before building the site around the optimiser.
  • next/script strategies are beforeInteractive, afterInteractive (the default) and lazyOnload for widgets that can wait.

FAQ

Tailwind or CSS Modules?
Both are supported and either is fine; mixing them in one project is what causes confusion. Tailwind suits teams that prefer styling in the markup, CSS Modules suit teams that want a stylesheet per component. Pick one per project.
Why is my remote image rejected?
The host is not in images.remotePatterns. Add the protocol and hostname, and keep the list narrow — it is an allowlist for the image optimiser, not a general proxy setting.

Setting up a project with create-next-app Streaming, Suspense and partial prerendering

Last refreshed 2026-09-18.