Styling, Images and Fonts · Lesson 16 of 24
Styling Next.js with Tailwind CSS and CSS Modules
Next.js supports every common way of writing CSS, with no configuration: global stylesheets, CSS Modules, Sass, and Tailwind CSS – which create-next-app sets up by default. This lesson covers the three you'll use most and shows what the browser actually ends up applying.
In this lesson
- Tailwind CSS v4: setup, utilities and theme tokens
- Global CSS
- CSS Modules for scoped styles
- Conditional classes and dark mode
Tailwind CSS v4
New apps already have Tailwind. The setup is a single line in your global stylesheet; there's no tailwind.config.js any more – customisation happens in CSS with @theme:
/* app/globals.css */
@import "tailwindcss";
@theme {
--color-brand: #087ea4; /* creates bg-brand, text-brand, border-brand… */
--font-display: "Georgia", serif; /* creates font-display */
}
// app/page.tsx
export default function Home() {
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="font-display text-3xl font-bold text-brand">Learn Next.js</h1>
<p className="mt-2 text-gray-600">24 lessons · free</p>
<a href="/start" className="mt-4 inline-block rounded-lg bg-brand px-5 py-2 text-white hover:opacity-90">
Start learning
</a>
</main>
);
}
Learn Next.js 24 lessons · free Start learning h1 → font-family: Georgia, serif; font-size: 30px; font-weight: 700; color: rgb(8, 126, 164) main → max-width: 672px; padding: 32px a → background-color: rgb(8, 126, 164); border-radius: 8px; padding: 8px 20px
Each utility class maps to one CSS declaration – text-3xl is 30px, p-8 is 32px – and your @theme tokens become utilities of their own. In production, Tailwind outputs only the classes your code uses, so the stylesheet stays small.
Global CSS
Import a stylesheet in the root layout and it applies to every page. Use it for base styles, CSS variables and third-party CSS – not for component styles, which can clash:
/* app/globals.css */
@import "tailwindcss";
:root {
--page-bg: #fafafa;
}
body {
background: var(--page-bg);
line-height: 1.6;
}
// app/page.tsx
export default function Home() {
return <p>Global styles apply to every page.</p>;
}
Global styles apply to every page. body → background-color: rgb(250, 250, 250); line-height: 25.6px
CSS Modules
A file ending in .module.css is scoped to the component that imports it: Next.js rewrites each class name to a unique one, so .card in one component can never clash with .card in another.
One-time fix for apps created with create-next-app 16
The starter's next.config.ts runs the Tailwind loader on every .css file and outputs it as plain CSS – including .module.css files, which then lose their scoping (the import returns no class names and the styles leak globally). We hit this while testing this lesson. Exclude module files from the rule:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
turbopack: {
rules: {
"*.css": {
condition: { not: { path: "*.module.css" } }, // leave CSS Modules alone
loaders: ["@tailwindcss/turbopack"],
as: "*.css",
},
},
},
};
export default nextConfig;
(Alternatively, use the PostCSS setup from the Next.js docs: @tailwindcss/postcss in a postcss.config.mjs, which handles both.) Now the module:
/* app/course-card.module.css */
.card {
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 16px;
}
.featured {
border-color: #087ea4;
}
// app/course-card.tsx
import styles from "./course-card.module.css";
export function CourseCard({ title, featured = false }: { title: string; featured?: boolean }) {
return <article className={featured ? `${styles.card} ${styles.featured}` : styles.card}>{title}</article>;
}
// app/page.tsx
import { CourseCard } from "./course-card";
export default function Home() {
return (
<main>
<CourseCard title="React.js" />
<CourseCard title="Next.js" featured />
</main>
);
}
React.js Next.js article → border-radius: 12px; padding: 16px; border-top-color: rgb(229, 231, 235) article:nth-child(2) → border-top-color: rgb(8, 126, 164)
Conditional classes
For Tailwind, building class strings with conditions is common. The tiny clsx package keeps it readable, and tailwind-merge resolves conflicts when a parent passes overriding classes:
import clsx from "clsx";
function Badge({ status }: { status: "draft" | "published" | "archived" }) {
return (
<span
className={clsx("rounded px-2 py-0.5 text-xs font-medium", {
"bg-yellow-100 text-yellow-800": status === "draft",
"bg-green-100 text-green-800": status === "published",
"bg-gray-100 text-gray-500 line-through": status === "archived",
})}
>
{status}
</span>
);
}
Dark mode
Tailwind's dark: variant follows the operating system setting by default:
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">…</div>
To let users choose, switch the variant to a class with @custom-variant dark (&:where(.dark, .dark *)); in your CSS and toggle a dark class on <html>. Set it before the page paints (for example from a cookie read in the root layout) so the page doesn't flash the wrong theme.
Which approach?
- Tailwind – the default for new Next.js projects and most component libraries (shadcn/ui).
- CSS Modules – when you prefer writing CSS, or for complex selectors and animations.
- Global CSS – resets, variables and third-party styles only.
- Runtime CSS-in-JS libraries need extra setup with Server Components; prefer the options above for new code.
Practice
Try it yourself
Add a
--color-accent: #e11d48theme token and use it for a "New" badge with white text and rounded corners.Show solution
/* app/globals.css */ @import "tailwindcss"; @theme { --color-accent: #e11d48; }// app/page.tsx export default function Home() { return <span className="rounded-full bg-accent px-3 py-1 text-sm text-white">New</span>; }New span → background-color: rgb(225, 29, 72); color: rgb(255, 255, 255); font-size: 14px
Quick check
Where do you customise Tailwind v4's colours and fonts?
In CSS, with an @theme block.
How does a CSS Module prevent class name clashes?
Next.js rewrites each class to a unique name; you reference it as styles.card.
Where should global CSS be imported?
In the root layout (or another layout).
Summary
- Tailwind v4: @import "tailwindcss" plus @theme tokens; utilities in JSX, unused classes removed.
- Global CSS for base styles; CSS Modules for scoped component styles.
- Use clsx for conditional classes and the dark: variant for dark mode.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.