Styling
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Next.js 16.3.8 (the latest release on 2026-10-03, when the facts and examples below were
checked; the official documentation reports @doc-version 16.3.8), React 19.3.0, Node.js 20.9.0 or later and
Tailwind CSS 4.3.3. It covers the App Router first and marks every place where the Pages Router differs. Styling is
almost independent of the caching model: the stylesheets are build artefacts served as static files, so the examples
behave identically with the previous model and with Cache Components (cacheComponents: true); every route below
was built under both models and served under the previous one. What does depend on the architecture is the split between Server Components and Client
Components, because a CSS-in-JS library that needs React context or hooks can only run in the second kind. Every example
was built with next build and served with next start on Next.js 16.3.8 and Node.js 24.16 in a scratch copy of the
Bookshelf catalogue that this section grows, and the generated HTML and CSS files were inspected with curl.
Next.js lets you import a .css file from a JavaScript or TypeScript module, as if CSS were code. The bundler (Turbopack
by default since Next.js 16, webpack on request) collects those imports, compiles them with PostCSS and Lightning CSS,
and, in a production build, concatenates and minifies them into a few code-split .css files. Your job is to choose the
authoring style; the official guide lists six: Tailwind CSS, CSS Modules, global CSS, external stylesheets, Sass and
CSS-in-JS. The sections below take them in turn, then the two topics that decide whether a large application stays
predictable: CSS ordering and the cssChunking option.
For the underlying language, see HTML and CSS; for the React-only view of the same choices
(plain CSS, modules, utility classes, libraries), see Styling and UI
Libraries. This page concentrates on what Next.js adds. For fonts, next/image and third-party scripts, which also
affect how a page looks while it loads, see the sibling page on images, fonts and scripts (written alongside this one);
for CSS-related performance, see the sibling performance page.
Choosing an approach
enough?} B -- yes --> T[Tailwind CSS v4
classes in JSX] B -- no --> C{Component-specific
rules?} C -- yes --> M[CSS Module
or .module.scss] C -- no --> G[Global CSS
in the root layout] M --> D{Runtime values
from props or state?} D -- yes --> V[CSS variables set with style=
or, in a Client Component, CSS-in-JS] D -- no --> Z[Done: zero JavaScript cost]
The official guide gives the same order of preference: use Tailwind for most styling, CSS Modules when utilities are not enough, and reserve global CSS for rules that are truly global (the Tailwind base layer, a reset, design tokens). CSS-in-JS comes last, because it is the only option that executes at runtime in the browser and the only one with restrictions in Server Components (see CSS-in-JS and the Server Component constraint).
| Approach | Works in Server Components | Cost | Typical use |
|---|---|---|---|
Global CSS |
Yes (import it in a layout) |
Static |
Reset, tokens, Tailwind base |
CSS Modules ( |
Yes |
Static |
Component-scoped rules |
Tailwind CSS v4 |
Yes |
Static |
Most layout and styling |
Sass |
Yes |
Compiled at build time to plain CSS |
Existing Sass code bases, mixins, loops |
Styled JSX, styled-components |
Only in Client Components (Styled JSX), or with a version that supports it (styled-components, see below) |
Style registry plus runtime in the browser |
Dynamic styles driven by props |
Emotion, most component libraries |
Only in Client Components |
Runtime, |
Libraries built on it (for example Chakra UI) |
Global CSS
A global stylesheet applies to every element it matches, wherever it is imported. In the App Router you import it in the
root layout so that every route receives it; in the Pages Router you import it in pages/_app.
body {
padding: 20px 20px 60px;
max-width: 680px;
margin: 0 auto;
}
// These styles apply to every route in the application
import './global.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
Source: Next.js: CSS, Global CSS
The Pages Router version imports the same stylesheet in pages/_app; the scratch app built and served it, the
<link> tag carried the marker attribute data-n-g ("global") and the page’s own stylesheet data-n-p:
import type { AppProps } from 'next/app'
import '../styles/globals.css'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
Three details matter.
-
Global styles can be imported from any layout, page or component in
app. That is permitted, but the official guide warns that Next.js integrates with React’s built-in stylesheet support (and therefore with Suspense), which means a stylesheet is currently not removed when you navigate away from the route that imported it. Rules from two different routes can then meet on the same page. Keep global CSS for rules that are meant to apply everywhere. -
In the Pages Router, import global CSS in
_app. The official page says to do so "to avoid conflicts". In my test on 16.3.8, importing a global stylesheet from a page instead built without error with both Turbopack and webpack (older Next.js versions refused it with the error documented at Global CSS Must Be in Your Custom App), so the build will not catch this for you; treat the_apprule as a convention to follow. -
CSS from
node_modulescan be imported anywhere inapp. The official guide showsimport 'bootstrap/dist/css/bootstrap.css'in the root layout. In the Pages Router, import library-wide stylesheets such as Bootstrap in_app, and component-specific ones (for example a dialog library’sstyles.css) in the component that needs them.
Plain <link> stylesheets with React 19
Besides import, React 19 lets any component render <link rel="stylesheet"> with a precedence attribute. React
hoists the tag into the <head>, de-duplicates it, and, when it appears under Suspense, holds back the content until the
stylesheet has loaded. In the scratch app, a Server Component rendering this tag produced exactly the tag below in the
HTML:
export default function Page() {
return (
<main>
<link rel="stylesheet" href="/promo.css" precedence="default" />
<p className="promo">Promo</p>
</main>
)
}
<link rel="stylesheet" href="/promo.css" data-precedence="default"/>
Use it for a stylesheet that is not part of your bundle (a file in public/, a stylesheet from a CDN). The official
guide states this is possible in React 19; the attribute reference is on react.dev.
Source: Next.js: CSS, External stylesheets and React: <link>
CSS Modules
A CSS Module is a stylesheet whose class names are rewritten to be unique, so the same name can be used in many files
without collisions. Name the file <name>.module.css, import it as an object, and use the object’s properties as class
names. The same works in app/ and in pages/.
.card {
border: 1px solid var(--card-border, #d4d4d4);
border-radius: 0.5rem;
padding: 1rem;
}
.title {
margin: 0;
font-size: 1.25rem;
}
/* composes: reuse another class from the same (or another) module */
.featured {
composes: card;
border-color: gold;
}
/* :global escapes local scoping for one selector */
.card :global(.badge) {
font-weight: 700;
}
import clsx from 'clsx'
import styles from './book-card.module.css'
export function BookCard({ title, featured = false }: { title: string; featured?: boolean }) {
return (
<article className={clsx(featured ? styles.featured : styles.card)}>
<h2 className={styles.title}>{title}</h2>
<span className="badge">New</span>
</article>
)
}
Rendered by a Server Component page, the generated HTML (shortened here) shows the hashed names; composes produced two
classes on the featured card, and the badge class stayed global because of :global:
<article class="book-card-module__sedPEq__card">
<h2 class="book-card-module__sedPEq__title">Dune</h2><span class="badge">New</span>
</article>
<article class="book-card-module__sedPEq__featured book-card-module__sedPEq__card">...
Source: Next.js: CSS, CSS Modules
Practical rules:
-
One module per component, named after it (
book-card.module.cssnext tobook-card.tsx). The official guide recommends a consistent naming convention. -
Selectors must be "pure": each rule should contain at least one local class or id. With webpack (
next build --webpack), a module containingbody { margin: 0; }fails the build withSelector "body" is not pure (pure selectors must contain at least one local class or id). With Turbopack in 16.3.8, the same file built without any error and emitted a plain, globalbody{margin:0}rule. Do not rely on that leniency: put element selectors in global CSS, or write:global(body). -
Combine classes with a small helper such as
clsx, or a template literal, instead of string concatenation. A module’s object exposes class names as properties, sostyles['shelf__item']is needed for names that are not valid identifiers. -
Pass a
classNameprop to let a parent adjust a child (see CSS ordering and merging, which shows the one pitfall of this pattern). -
Values that change per instance belong in a CSS custom property set through
style, for examplestyle={{ '--card-border': color } as React.CSSProperties}, and are read in the module withvar(--card-border).
Tailwind CSS v4
Tailwind is the first option in the official guide, and it is what create-next-app scaffolds by default (the scratch
app was created with it). Version 4 is configured in CSS rather than in a JavaScript file. Next.js uses its PostCSS
plugin:
npm install -D tailwindcss @tailwindcss/postcss
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
@import 'tailwindcss';
Import globals.css in the root layout (as in Global CSS), and utility classes work in every Server and Client
Component:
export default function Page() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<h1 className="text-4xl font-bold">Welcome to Next.js!</h1>
</main>
)
}
Source: Next.js: CSS, Tailwind CSS
In the Pages Router the stylesheet is imported in pages/_app instead. Tailwind scans your source files for class names
and writes only the utilities it finds, so the output stays small whatever the size of the application. The full
treatment lives in the Tailwind section: installation and browser support,
the @theme directive, dark mode, and
production builds. The v3 to v4 changes (the CSS-first configuration,
@import 'tailwindcss' replacing the three @tailwind directives) are in
Upgrading from v3 to v4.
Tailwind inside a CSS Module
A CSS Module is compiled separately from your global stylesheet, so it does not know your theme. To use @apply or
theme variables inside one, point it at the main stylesheet with @reference. The scratch build confirmed that the
module below compiles to a normal hashed class holding the resolved values, with the dark-mode media query applied:
@reference "../app/globals.css";
.price {
@apply font-mono text-lg text-emerald-700;
}
@media (prefers-color-scheme: dark) {
.price {
@apply text-emerald-300;
}
}
Prefer utility classes in the markup and reach for @apply only when a rule must live in a module (a third-party
component you cannot add classes to, for instance); otherwise each module re-processes the theme.
Tailwind v3 and old browsers
Tailwind v4 relies on modern CSS (@property, color-mix(), cascade layers). The Tailwind documentation lists the
supported browsers as Safari 16.4, Chrome 111 and Firefox 128 or later. If you need to support older browsers, the
Next.js documentation keeps a separate guide for Tailwind v3
(https://nextjs.org/docs/app/guides/tailwind-v3-css).
Sass
Next.js compiles .scss and .sass files after you install the sass package; .module.scss and .module.sass give
component-scoped Sass. No other configuration is required.
npm install --save-dev sass
Start with the .scss syntax (a superset of CSS); the indented .sass syntax is optional. Sass runs at build time and
leaves plain CSS behind, so it works in Server Components with no runtime cost.
@use "sass:color";
$primary-color: #64ff00;
$gap: 0.5rem;
.shelf {
--shelf-accent: #{$primary-color}; // Sass value exposed as a CSS custom property
display: flex;
gap: $gap;
border: 2px solid $brand; // $brand comes from sassOptions.additionalData
&__item {
color: color.adjust($primary-color, $lightness: -30%);
border-bottom: 1px solid var(--shelf-accent);
}
}
import styles from '@/components/shelf.module.scss'
export default function Page() {
return (
<ul className={styles.shelf}>
<li className={styles['shelf__item']}>Dune</li>
<li className={styles['shelf__item']}>Solaris</li>
</ul>
)
}
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
sassOptions: {
// prepended to every Sass file; here it defines $brand
additionalData: `$brand: #0b5fff;`,
},
}
export default nextConfig
sassOptions configures the compiler: additionalData prepends code to every file, and implementation selects the
Sass implementation (the default is the sass package; 'sass-embedded' is an alternative). Only implementation is
typed, because Next.js does not maintain the other properties. The custom Sass functions option is available only with
webpack: Turbopack cannot call JavaScript functions passed through it.
Source: Next.js: Sass and Next.js: sassOptions
For the language itself, see the Sass Reference, and for compiling Sass outside Next.js Sass compilation.
Exporting Sass variables to TypeScript
The official Sass guide shows a CSS Module that exports a Sass variable with :export { primaryColor: $primary-color; }
and reads it from the component as variables.primaryColor. In my test on 16.3.8:
-
with
next build --webpack, the value was available (style="border-color:#64ff00"was rendered); -
with the default Turbopack build, the build printed the warning
'export' is not recognized as a valid pseudo-classandstyles.primaryColorwas missing, so the style was silently dropped.
The robust alternative, used in the example above, is to expose the value as a CSS custom property (--shelf-accent:
#{$primary-color}) and consume it in CSS with var(). That works in every bundler and also in dark mode and media
queries, without crossing the CSS/JavaScript boundary. If JavaScript really needs the value, define it once in
TypeScript and pass it to Sass through additionalData.
The Sass darken() function and the other global colour functions are deprecated by Sass; the scratch build warned about
it until the example switched to color.adjust() from sass:color. Use the module system (@use) rather than @import
in new code (see Sass: @import deprecation).
Source: Next.js: Sass, Sass Variables and Sass: color functions
CSS-in-JS and the Server Component constraint
CSS-in-JS libraries generate CSS from JavaScript at render time. That model collides with Server Components, which cannot use React state, effects or context, and which are rendered once on the server without re-running in the browser. The official page opens with a warning: using CSS-in-JS with newer React features such as Server Components and streaming requires the library authors to support the latest React, including concurrent rendering.
The rules, in order of importance:
-
A CSS-in-JS library that relies on context or hooks works only in Client Components (files with
"use client", or modules imported by one). Importing it in a Server Component fails. I imported@emotion/styledin a Server Component page andnext buildfailed at "Collecting page data" withTypeError: g.createContext is not a function. The usual fix is to put the styled component in a file marked"use client", as the next example does. -
Client Components are still rendered on the server for the first HTML. So the library needs to collect the CSS produced during that render and put it in the
<head>before the HTML that uses it, otherwise the first paint is unstyled. Next.js offersuseServerInsertedHTML(imported fromnext/navigation) for that; the official configuration is a three-step opt-in: a style registry that collects the rules,useServerInsertedHTMLto inject them, and a Client Component that wraps the application with the registry. -
Libraries that support the
appdirectory are listed by the official page (alphabetical): Ant Design, Chakra UI, Fluent UI, Kuma UI, MUI Material and Joy, Panda CSS, Styled JSX, styled-components, StyleX, Tamagui, tss-react and vanilla-extract. Emotion was listed as "working on support" in 16.3.8. -
Some libraries now ship their own Server Component support. In the scratch app,
styled-components6.5.3 (latest on npm on 2026-10-03) rendered a styled component directly in a Server Component page with no"use client"and no registry, and put a<style data-styled>element and a generated class in the HTML. Check each library’s documentation for the supported configuration and versions rather than assuming the older registry recipe is still needed.
CSS-in-JS costs more than the alternatives: runtime JavaScript in the client bundle, style recalculation when props
change, and a registry boilerplate. For values that change at runtime, a CSS custom property set from style, or a
data- attribute that CSS selects on, usually achieves the same result without a library. Keep such components as
small Client Components at the leaves, and keep pages and layouts as Server Components (see
Server and Client Components).
Styled JSX
Styled JSX is bundled with Next.js (styled-jsx 5.1.6 was resolved by next@16.3.8) and provides scoped CSS inside a
component with a <style jsx> element. In the App Router it is supported only in Client Components, and the official
guide requires version 5.1.0 or later and a registry:
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { StyleRegistry, createStyleRegistry } from 'styled-jsx'
export default function StyledJsxRegistry({
children,
}: {
children: React.ReactNode
}) {
// Only create stylesheet once with lazy initial state
const [jsxStyleRegistry] = useState(() => createStyleRegistry())
useServerInsertedHTML(() => {
const styles = jsxStyleRegistry.styles()
jsxStyleRegistry.flush()
return <>{styles}</>
})
return <StyleRegistry registry={jsxStyleRegistry}>{children}</StyleRegistry>
}
import StyledJsxRegistry from './registry'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<StyledJsxRegistry>{children}</StyledJsxRegistry>
</body>
</html>
)
}
A Client Component page using it (the scratch app wrapped one route in the registry through a segment layout) rendered the scoped rule inside the HTML, so the first paint is styled:
'use client'
export default function Page() {
return (
<div className="shelf">
<p>Styled JSX</p>
<style jsx>{`
.shelf p {
color: rebeccapurple;
}
`}</style>
</div>
)
}
<style id="__jsx-a138ec66f16310cf">.shelf.jsx-a138ec66f16310cf p.jsx-a138ec66f16310cf{color:#639}</style>
<div class="jsx-a138ec66f16310cf shelf"><p class="jsx-a138ec66f16310cf">Styled JSX</p></div>
Source: Next.js: CSS-in-JS, styled-jsx
In the Pages Router, no registry is needed: Next.js bundles styled-jsx, and <style jsx> works in any page or
component; <style jsx global> makes the rules global. The official Pages guide still describes this setup.
styled-components
styled-components version 6 or later needs the Next.js compiler transform for readable class names and a registry that
flushes the collected rules into the document. Enable the transform in next.config.ts:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
compiler: {
styledComponents: true,
},
}
export default nextConfig
Then create the registry as a Client Component, and wrap children in the layout with it:
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
export default function StyledComponentsRegistry({
children,
}: {
children: React.ReactNode
}) {
// Only create stylesheet once with lazy initial state
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement()
styledComponentsStyleSheet.instance.clearTag()
return <>{styles}</>
})
if (typeof window !== 'undefined') return <>{children}</>
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
)
}
'use client'
import styled from 'styled-components'
export const Title = styled.h1`
color: crimson;
`
A Server Component page can then render the Client Component Title; the build succeeded and the HTML contained the
extracted rule and a class carrying the component’s name (title__Title-sc-…) thanks to the compiler transform:
<style data-styled="" data-styled-version="6.5.3">.eHoxLb{color:crimson;}/*!sc*/</style>
<h1 class="title__Title-sc-83983c48-0 eHoxLb">Styled Components</h1>
The official notes explain the design: styles are extracted into a global registry and flushed into the <head> before
any content that uses them; during streaming the styles of each chunk are appended; after hydration, the library takes
over and injects any further dynamic styles; and the registry is a Client Component at the top of the tree because that
avoids regenerating styles on later server renders and keeps them out of the Server Component payload.
Other libraries
For Emotion, MUI, Chakra UI, Ant Design and the rest, follow the library’s own Next.js App Router guide; the Next.js
page links each one. They all follow the same pattern: a Client Component provider (often generated by the library’s
CLI) that creates the cache or registry and calls useServerInsertedHTML. Zero-runtime approaches such as Tailwind, which emit
static CSS at build time, need no registry and work in Server Components; the same is generally true of the libraries
that generate their CSS at build time (vanilla-extract, Panda CSS, StyleX), but check each one’s guide.
CSS ordering and merging
When two rules have the same specificity, the one that comes later in the final stylesheet wins. Next.js builds that order from your import order: in production, it merges stylesheets into chunks and the order of the CSS follows the order in which the modules are imported.
The official example is a page that imports a BaseButton component before its own module. The base button’s CSS is
therefore emitted first, and the page can override it:
import { BaseButton } from '@/components/order/base-button'
import styles from './page.module.css'
export default function Page() {
return <BaseButton className={styles.primary} />
}
import styles from './base-button.module.css'
export function BaseButton({ className = '' }: { className?: string }) {
return <button className={`${styles.primary} ${className}`}>Buy</button>
}
The built CSS chunk, fetched with curl, contained the base rule first and the page rule second, so the page’s crimson
background overrode the base button’s navy:
.base-button-module__rowGPa__primary{color:#fff;background:navy;padding:.5rem}
.page-module__ApWZTG__primary{background:#dc143c}
If the page imported its module before importing BaseButton, the order would flip and the override would stop
working. This is the typical source of "it worked in development but not in production" bugs, because the order of CSS
in next dev can differ from the one in next build: always check the production build. The official recommendations
for keeping the order predictable are:
-
contain CSS imports in a single JavaScript or TypeScript entry file where you can;
-
import global styles and Tailwind in the root of the application;
-
use Tailwind for most styling and CSS Modules for component-specific styles;
-
use a consistent naming convention for modules (
<name>.module.cssnext to<name>.tsx); -
extract shared styles into shared components so that duplicate imports disappear;
-
turn off linters or formatters that auto-sort imports, such as ESLint’s
sort-imports, because sorting imports reorders the CSS; -
tune chunking with
cssChunkingwhen needed.
A related development-versus-production difference: in next dev, CSS changes are applied instantly by Fast Refresh;
in next build, all CSS is concatenated into many minified, code-split .css files, so that each route loads the
minimum; CSS still loads with JavaScript disabled in production (a JavaScript-free page is still styled), but
development requires JavaScript for Fast Refresh.
The cssChunking option
experimental.cssChunking controls how the bundler splits and re-orders the CSS files into chunks. The page in the
official documentation is labelled experimental in 16.3.8, even though the default value is true. The accepted
values depend on the bundler:
| Value | Bundler | Effect |
|---|---|---|
|
Turbopack and webpack |
Merges CSS files whenever possible, deducing explicit and implicit dependencies from import order, to reduce the number of chunks and requests. |
|
webpack only |
Does not merge or re-order CSS files. |
|
webpack only |
Loads CSS files in exactly the order they are imported, at the cost of more chunks and requests. |
|
Turbopack only |
A cost-based graph algorithm that groups CSS across routes, balancing bytes downloaded and number of requests. Accepts an object with |
import type { NextConfig } from 'next'
const nextConfig = {
experimental: {
cssChunking: {
type: 'graph',
requestCost: 100000,
weightDistribution: 0.1,
},
},
} satisfies NextConfig
export default nextConfig
The string form cssChunking: 'graph' uses the defaults. I built the scratch app with 'graph' on Turbopack and with
'strict' on webpack (next build --webpack); both built successfully. For this small application the routes loaded
two stylesheets either way, as with the default, so the difference only shows in larger applications.
How to decide:
-
Keep the default unless a measurement or a bug says otherwise.
-
Use
'strict'(webpack) for correctness. Ifa.cssandb.cssare imported in different orders in different files andb.cssdepends ona.css, the default may merge them in either order;'strict'prevents the merge. -
Use
'graph'(Turbopack) for performance. It cuts the unused CSS a route downloads at the price of more requests. RaiserequestCostfor fewer, larger shared chunks; lower it towards0for routes that download closer to only what they import. RaiseweightDistributionto favour routes that import little CSS. -
Diagnose first. Lighthouse reports "Reduce unused CSS", and the Chrome DevTools Coverage panel shows used and unused CSS per stylesheet. Rules that only apply on
:hoveror on classes toggled by JavaScript count as unused until you trigger them.
Source: Next.js: cssChunking
Inline CSS (inlineCss)
Another experimental option, experimental.inlineCss: true, makes Next.js emit <style> elements in the <head> in
every place where it would normally emit a <link rel="stylesheet">. In the scratch app, the routes that had two
<link> tags with the default configuration had none and one <style> element after the production build with
inlineCss enabled. The trade-off is documented:
-
Enable for small, atomic CSS (Tailwind) when first-visit performance matters, because inlining removes the request waterfall for new visitors and helps First Contentful Paint and Largest Contentful Paint, especially on slow networks.
-
Skip when returning visitors are common or the CSS is large: inlined CSS cannot be cached separately from the HTML, so it is downloaded again on every page load.
-
The official page lists known limitations: it is applied globally and not per page; styles are duplicated during the initial load (once in
<style>for SSR and once in the RSC payload); prerendered pages reached by client navigation use<link>tags; and it only works in production builds, not innext dev.
Source: Next.js: inlineCss
PostCSS and Lightning CSS
Next.js compiles CSS with PostCSS (reading postcss.config.* at the project root, which Tailwind needs) and, on
Turbopack, with Lightning CSS, a Rust CSS transformer and minifier. experimental.useLightningcss applies only to
webpack, where it is off by default, and has no effect on Turbopack, which always uses Lightning CSS. In a monorepo,
experimental.turbopackLocalPostcssConfig: true makes Turbopack look for postcss.config.js starting at the CSS file’s
directory and falling back to the root, instead of the root first. Both are experimental.
App Router and Pages Router at a glance
| Topic | App Router | Pages Router |
|---|---|---|
Global CSS |
Import in the root layout (or any layout, page or component) |
Import in |
CSS Modules |
|
Same, imported by any component in |
Tailwind and Sass |
Same configuration ( |
Same configuration; Tailwind imported in |
Styled JSX |
Client Components only, with a registry and |
Works in any component, no registry |
Other CSS-in-JS |
Registry in the root layout; Client Components, unless the library supports Server Components |
Library-specific setup; see the examples linked from the Pages guide |
CSS from |
Import in any component in |
Library-wide in |
What changed recently
| Version | Change | Impact |
|---|---|---|
Next.js 16 (2025-10-21) |
Turbopack is the default bundler for |
Sass, PostCSS and CSS Modules run on Turbopack; verify |
Next.js 16 |
|
The |
Tailwind CSS 4 (stable since early 2025) |
CSS-first configuration; |
See the Tailwind upgrade page. |
React 19 |
Native support for |
Plain stylesheet tags are a supported alternative to |
Next.js 15 |
|
Affects the font CSS variables used in themes (covered on the sibling fonts page). |
|
Both still documented as experimental in 16.3.8; |
Verify behaviour after each upgrade. |
Where the books are outdated
The two reference books were written against Next.js 9 to 13 and React 17 and 18. Their styling chapters are still conceptually right (global CSS, CSS Modules, Sass, a utility framework, CSS-in-JS), but several details need updating:
| Book approach | Today |
|---|---|
Styled JSX as the default scoped-styles tool, used in any page component. |
Still bundled ( |
Chakra UI v1 ( |
Chakra UI is now at 3.x ( |
Tailwind v2 or v3 with |
Tailwind v4 (4.3.3 on npm): |
Importing global CSS from |
Still the recommended convention, though 16.3.8 no longer refuses other imports (see above). |
Sass variables exported with |
Works with webpack; on the Turbopack default of Next.js 16, expose values as CSS custom properties instead. |
|
In the App Router use |