LearnFrenzy
Sign in Take a free test

Styling, Images and Fonts · Lesson 16 of 24

Styling Next.js with Tailwind CSS and CSS Modules

5 min read Updated Nov 8, 2026 Saurabh Samir

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>
  );
}

Page and computed styles

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>;
}

Page and computed styles

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>
  );
}

Page and computed styles

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

  1. Add a --color-accent: #e11d48 theme 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>;
    }

    Page and computed styles

    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.

Next lesson

Search LearnFrenzy

Esc

Quick links