EthicsComputerCourses
Join training +91 90532 10052

Styling: CSS Modules & Tailwind

Next.js · Lesson 20 of 27 Advanced 7 min read Updated

Next.js does not force one way of styling. You can write ordinary CSS, scope it to one component with CSS Modules, use Tailwind's utility classes, or combine them. The default project uses Tailwind CSS plus a small global stylesheet, and that combination works well for most teams.

In this lesson you will use global CSS, write CSS Modules, set up and use Tailwind CSS v4, and learn when Sass and CSS-in-JS make sense with Server Components.

Global CSS

A global stylesheet applies to every page. Import it once in the root layout:

CSS
/* app/globals.css */
:root {
  --brand: #111827;
  --accent: #f59e0b;
}

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  color: var(--brand);
}

@media (prefers-color-scheme: dark) {
  :root { --brand: #f3f4f6; }
  body { background: #0b0f19; }
}
React JSX
// app/layout.jsx
import "./globals.css";

Keep global CSS for things that really are global: CSS variables, base styles for body and headings, and Tailwind's import. Next.js does not remove a stylesheet when you navigate away from the page that imported it, so page-specific rules in global files can clash later. For component styles, use CSS Modules or Tailwind.

CSS Modules

A file ending in .module.css is a CSS Module. When you import it, you get an object whose keys are your class names, and Next.js rewrites each class to a unique name. Two components can both use .title without affecting each other.

CSS
/* app/courses/CourseCard.module.css */
.card {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 16px;
}
.title {
  margin: 0 0 8px;
  font-size: 1.25rem;
}
.free {
  color: #15803d;
}
React JSX
// app/courses/CourseCard.jsx
import styles from "./CourseCard.module.css";

export default function CourseCard({ title, fee }) {
  return (
    <article className={styles.card}>
      <h2 className={styles.title}>{title}</h2>
      <p className={fee === 0 ? styles.free : undefined}>{fee === 0 ? "Free" : `₹${fee}`}</p>
    </article>
  );
}

In the browser the class might become something like CourseCard_title__x1y2z in development; production builds use even shorter names. CSS Modules work in Server and Client Components.

The runnable example imitates what CSS Modules do: it gives two components the same class name, title, and makes each unique by adding a component prefix, so the styles never collide:

React JSXTry it yourself
import { createRoot } from "react-dom/client";

function cssModule(componentName, css) {
  const map = {};
  const scoped = css.replace(/\.([a-zA-Z][\w-]*)/g, (_, cls) => {
    map[cls] = `${componentName}_${cls}__${componentName.length}${cls.length}x`;
    return "." + map[cls];
  });
  const style = document.createElement("style");
  style.textContent = scoped;
  document.head.appendChild(style);
  return map;
}

const cardStyles = cssModule("CourseCard", ".title { color: #1d4ed8; font-size: 22px; }");
const noticeStyles = cssModule("Notice", ".title { color: #b91c1c; font-size: 16px; text-transform: uppercase; }");

console.log("CourseCard title class:", cardStyles.title);
console.log("Notice title class:", noticeStyles.title);

createRoot(document.getElementById("root")).render(
  <>
    <h2 className={cardStyles.title}>React Basics</h2>
    <p className={noticeStyles.title}>Admissions close on 30 November</p>
  </>
);
Output
CourseCard title class: CourseCard_title__105x
Notice title class: Notice_title__65x

Next.js uses a real hash instead of these simple numbers, but the result is the same: one blue title and one red title, from two classes both written as .title.

Tailwind CSS v4

Tailwind gives you small utility classes such as p-4, rounded-lg and text-amber-600 that you combine in className. It is chosen by default in create-next-app. To add it to an existing project, the Next.js docs describe this setup:

Terminal
npm install -D tailwindcss @tailwindcss/postcss
JavaScript
// postcss.config.mjs
export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};
CSS
/* app/globals.css */
@import "tailwindcss";

Projects generated by create-next-app 16.4 may instead connect Tailwind through a Turbopack rule in next.config.ts (using the @tailwindcss/turbopack package); either way, globals.css starts with @import "tailwindcss" and you use the same classes. There is no tailwind.config.js in v4 by default: you customise the theme in CSS with @theme. The Tailwind tutorial covers the classes in depth.

This runnable page uses Tailwind's browser build, which is meant only for learning and quick prototypes; real projects use the setup above:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Tailwind course card</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="bg-gray-50 p-6">
  <article class="max-w-sm rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
    <p class="text-xs font-semibold uppercase tracking-wide text-amber-600">Weekend batch</p>
    <h2 class="mt-1 text-xl font-bold text-gray-900">Next.js Basics</h2>
    <p class="mt-2 text-gray-600">App Router, Server Components and Server Actions in 6 weeks.</p>
    <div class="mt-4 flex items-center justify-between">
      <span class="text-lg font-semibold">₹1,499</span>
      <button class="rounded-lg bg-gray-900 px-4 py-2 text-white hover:bg-gray-700">Enrol</button>
    </div>
  </article>
</body>
</html>

In Next.js you write the same classes on JSX elements with className instead of class. For conditional classes, build the string in JavaScript: className={`rounded px-3 ${active ? "bg-gray-900 text-white" : "bg-white"}`}.

Sass and CSS-in-JS

  • Sass: install it with npm install -D sass and use .scss files, including .module.scss. Next.js compiles them automatically.
  • CSS-in-JS libraries (which build styles from JavaScript at runtime) only work in Client Components, and some need extra setup to work with streaming. Because Server Components cannot use them, most new App Router projects prefer CSS Modules or Tailwind, which produce plain CSS files.

How CSS order works

In production, Next.js merges and splits your CSS into small, minified files per route, and CSS loads even without JavaScript. The order of rules follows the order of your imports. Keep global and Tailwind imports in the root layout, and import component CSS inside the components that use it. If something looks different after next build, check import order, because development can order CSS slightly differently.

Common mistakes

  • Writing class instead of className. JSX uses className.
  • Global class names for components. A .card rule in globals.css affects every .card in the app. Use a CSS Module.
  • Using a CSS-in-JS library in a Server Component. It needs "use client", which spreads client code. Prefer CSS Modules or Tailwind.
  • Building Tailwind class names from pieces. bg-${colour}-500 cannot be detected by Tailwind's scanner. Write full class names such as bg-red-500.

Key points

  • Import global CSS once in the root layout and keep it truly global.
  • .module.css files give locally scoped class names through an imported styles object.
  • Tailwind CSS v4 is set up with @import "tailwindcss" in globals.css plus the PostCSS (or Turbopack) integration.
  • Sass works after installing sass; CSS-in-JS needs Client Components.
  • CSS order follows import order; check the production build.

Exercise

Create a Notice component for a school website with a CSS Module: a yellow box with a left border, a bold title and a smaller date line. Then create the same box using Tailwind classes only. Show both versions in one page.

Show solution
CSS
/* app/_components/Notice.module.css */
.box { background: #fef9c3; border-left: 4px solid #ca8a04; padding: 12px 16px; }
.title { font-weight: 700; margin: 0; }
.date { font-size: 0.85rem; color: #6b7280; margin: 4px 0 0; }
React JSX
// app/_components/Notice.jsx
import styles from "./Notice.module.css";

export function Notice({ title, date }) {
  return (
    <div className={styles.box}>
      <p className={styles.title}>{title}</p>
      <p className={styles.date}>{date}</p>
    </div>
  );
}

export function NoticeTw({ title, date }) {
  return (
    <div className="border-l-4 border-yellow-600 bg-yellow-100 px-4 py-3">
      <p className="m-0 font-bold">{title}</p>
      <p className="mt-1 text-sm text-gray-500">{date}</p>
    </div>
  );
}

// app/notices/page.jsx
import { Notice, NoticeTw } from "@/app/_components/Notice";

export default function NoticesPage() {
  return (
    <>
      <Notice title="PTM on Saturday" date="15 November" />
      <NoticeTw title="Sports day" date="22 November" />
    </>
  );
}

Both boxes look alike. The CSS Module keeps styles in a separate file with scoped names, while the Tailwind version keeps them in the markup.

Quick Quiz

1. What makes a CSS file a CSS Module?

2. How do you use the class .title from Card.module.css in JSX?

3. What line goes at the top of globals.css for Tailwind CSS v4?

4. Why do many App Router projects avoid runtime CSS-in-JS libraries?

Learn Next.js with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call