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-appwires 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/fontdownloads 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/imageneeds eitherwidthandheight, orfillinside 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. sizestells 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/scriptstrategies arebeforeInteractive,afterInteractive(the default) andlazyOnloadfor 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.Related
Setting up a project with create-next-app Streaming, Suspense and partial prerendering
Last refreshed 2026-09-18.