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

flowchart TD A[Need to style a component] --> B{Utility classes
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).

Table 1. Where each approach runs and what it costs
Approach Works in Server Components Cost Typical use

Global CSS

Yes (import it in a layout)

Static .css file, render-blocking <link>

Reset, tokens, Tailwind base

CSS Modules (.module.css, .module.scss)

Yes

Static .css file with hashed class names; no runtime

Component-scoped rules

Tailwind CSS v4

Yes

Static .css file containing only the used utilities

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, createContext needed

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.

app/global.css
body {
  padding: 20px 20px 60px;
  max-width: 680px;
  margin: 0 auto;
}
app/layout.tsx
// 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>
  )
}

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:

pages/_app.tsx
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 _app rule as a convention to follow.

  • CSS from node_modules can be imported anywhere in app. The official guide shows import '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’s styles.css) in the component that needs them.

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:

app/lab/link/page.tsx
export default function Page() {
  return (
    <main>
      <link rel="stylesheet" href="/promo.css" precedence="default" />
      <p className="promo">Promo</p>
    </main>
  )
}
Rendered HTML (excerpt)
<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.

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

components/book-card.module.css
.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;
}
components/book-card.tsx
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">...

Practical rules:

  • One module per component, named after it (book-card.module.css next to book-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 containing body { margin: 0; } fails the build with Selector "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, global body{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, so styles['shelf__item'] is needed for names that are not valid identifiers.

  • Pass a className prop 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 example style={{ '--card-border': color } as React.CSSProperties}, and are read in the module with var(--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
postcss.config.mjs
export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}
app/globals.css
@import 'tailwindcss';

Import globals.css in the root layout (as in Global CSS), and utility classes work in every Server and Client Component:

app/page.tsx
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>
  )
}

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:

components/price.module.css
@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.

components/shelf.module.scss
@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);
  }
}
app/lab/sass/page.tsx
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>
  )
}
next.config.ts
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.

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-class and styles.primaryColor was 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).

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:

  1. 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/styled in a Server Component page and next build failed at "Collecting page data" with TypeError: 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.

  2. 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 offers useServerInsertedHTML (imported from next/navigation) for that; the official configuration is a three-step opt-in: a style registry that collects the rules, useServerInsertedHTML to inject them, and a Client Component that wraps the application with the registry.

  3. Libraries that support the app directory 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.

  4. Some libraries now ship their own Server Component support. In the scratch app, styled-components 6.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:

app/registry.tsx
'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>
}
app/layout.tsx
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:

app/lab/jsx/page.tsx
'use client'

export default function Page() {
  return (
    <div className="shelf">
      <p>Styled JSX</p>
      <style jsx>{`
        .shelf p {
          color: rebeccapurple;
        }
      `}</style>
    </div>
  )
}
Rendered HTML (excerpt)
<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>

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:

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:

lib/registry.tsx
'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>
  )
}
app/lab/sc/title.tsx
'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:

Rendered HTML (excerpt)
<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:

app/lab/order/page.tsx
import { BaseButton } from '@/components/order/base-button'
import styles from './page.module.css'

export default function Page() {
  return <BaseButton className={styles.primary} />
}
components/order/base-button.tsx
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:

Built chunk (excerpt)
.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.css next 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 cssChunking when 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:

Table 2. experimental.cssChunking values in Next.js 16.3
Value Bundler Effect

true (default)

Turbopack and webpack

Merges CSS files whenever possible, deducing explicit and implicit dependencies from import order, to reduce the number of chunks and requests.

false

webpack only

Does not merge or re-order CSS files.

'strict'

webpack only

Loads CSS files in exactly the order they are imported, at the cost of more chunks and requests.

'graph'

Turbopack only

A cost-based graph algorithm that groups CSS across routes, balancing bytes downloaded and number of requests. Accepts an object with requestCost (default 20000 bytes) and weightDistribution (default 0.1).

next.config.ts
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. If a.css and b.css are imported in different orders in different files and b.css depends on a.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. Raise requestCost for fewer, larger shared chunks; lower it towards 0 for routes that download closer to only what they import. Raise weightDistribution to 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 :hover or on classes toggled by JavaScript count as unused until you trigger them.

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 in next dev.

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

Table 3. Differences in styling between the two routers
Topic App Router Pages Router

Global CSS

Import in the root layout (or any layout, page or component)

Import in pages/_app

CSS Modules

.module.css imported by any component in app

Same, imported by any component in pages

Tailwind and Sass

Same configuration (@tailwindcss/postcss, sass)

Same configuration; Tailwind imported in _app

Styled JSX

Client Components only, with a registry and useServerInsertedHTML

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 node_modules

Import in any component in app

Library-wide in _app, component-specific in the component

What changed recently

Table 4. Styling changes by version
Version Change Impact

Next.js 16 (2025-10-21)

Turbopack is the default bundler for next dev and next build. Custom webpack configuration fails the build unless you pass --webpack.

Sass, PostCSS and CSS Modules run on Turbopack; verify :export Sass variables and non-pure module selectors (see above).

Next.js 16

next lint and the eslint config in next.config were removed.

The sort-imports advice now concerns your own ESLint setup.

Tailwind CSS 4 (stable since early 2025)

CSS-first configuration; @import 'tailwindcss'; @tailwindcss/postcss is a separate package; @reference for modules.

See the Tailwind upgrade page.

React 19

Native support for <link rel="stylesheet" precedence> and hoisting of <style>/<link> into <head>.

Plain stylesheet tags are a supported alternative to import.

Next.js 15

@next/font removed in favour of next/font.

Affects the font CSS variables used in themes (covered on the sibling fonts page).

cssChunking, inlineCss

Both still documented as experimental in 16.3.8; cssChunking gained the Turbopack-only 'graph' strategy with requestCost and weightDistribution.

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:

Table 5. Updating the books' styling material
Book approach Today

Styled JSX as the default scoped-styles tool, used in any page component.

Still bundled (styled-jsx 5.1.6 with Next.js 16.3.8), but in the App Router it works only in Client Components and needs the registry with useServerInsertedHTML shown above. The Pages Router keeps the original no-registry usage. CSS Modules or Tailwind are the recommended defaults for new code.

Chakra UI v1 (ChakraProvider with theme, extendTheme, useColorMode, wrapped in pages/_app).

Chakra UI is now at 3.x (@chakra-ui/react 3.37.0 on npm, 2026-10-03). Its Next.js App Router guide generates a provider component that composes ChakraProvider with next-themes for colour mode, wraps the root layout with it, and runs as a Client Component. Chakra v3 is a rewrite (a new styling system and component APIs), so code written for v1 does not carry over: follow the Chakra UI migration guide rather than patching imports.

Tailwind v2 or v3 with tailwind.config.js, purge or content arrays, three @tailwind directives and postcss.config.js listing tailwindcss and autoprefixer.

Tailwind v4 (4.3.3 on npm): @import 'tailwindcss', a single @tailwindcss/postcss plugin, configuration with @theme in CSS, automatic content detection. See the upgrade page of the Tailwind section.

Importing global CSS from _app.js only, or the Pages Router error for global CSS in components.

Still the recommended convention, though 16.3.8 no longer refuses other imports (see above).

Sass variables exported with :export and read from JavaScript.

Works with webpack; on the Turbopack default of Next.js 16, expose values as CSS custom properties instead.

next/head and <link> in _document for external stylesheets.

In the App Router use import or React 19 <link rel="stylesheet" precedence>.