Lesson 16 of 2222 min read

Styling in Next.js: CSS Modules, Tailwind CSS & Styled Components

Compare the three main styling approaches in Next.js — CSS Modules, Tailwind CSS, and Styled Components — and learn how to set up CSS Modules.

Author: CodersNexus

Styling in Next.js: CSS Modules, Tailwind CSS & Styled Components

Next.js doesn't force you into a single way of styling your application — it supports several approaches out of the box, and understanding the tradeoffs between them is one of the first practical decisions you'll make on any real project. Plain global CSS works but doesn't scale well past a small site, since class names can collide across files with no built-in protection.

This lesson introduces the three most common styling approaches you'll encounter in Next.js projects: CSS Modules, which give you automatically scoped class names using regular CSS syntax; Tailwind CSS, a utility-first framework where you compose designs directly in your markup using pre-defined classes; and Styled Components, a CSS-in-JS library where styles are written directly inside your JavaScript/TypeScript files, colocated with the components they style. We'll set up CSS Modules concretely and compare all three so you can make an informed choice for your own projects.

Learning Objectives

  • Explain the core problem CSS Modules, Tailwind, and Styled Components each solve.
  • Set up and use CSS Modules for automatically scoped component styles.
  • Understand the utility-first philosophy behind Tailwind CSS at a conceptual level.
  • Understand the CSS-in-JS philosophy behind Styled Components at a conceptual level.
  • Compare all three approaches to make an informed styling choice for a project.

Core Definitions

  • CSS Modules: A CSS file naming convention (*.module.css) supported natively by Next.js, where class names are automatically scoped locally to the component that imports them, preventing naming collisions.
  • Global CSS: Regular CSS applied site-wide, without any automatic scoping, typically imported once in the root layout.
  • Tailwind CSS: A utility-first CSS framework providing small, single-purpose classes (like flex, p-4, text-lg) that are composed directly in markup instead of writing custom CSS rules.
  • Styled Components: A CSS-in-JS library that lets you write actual CSS inside JavaScript/TypeScript template literals, generating uniquely scoped class names automatically, colocated with your component code.
  • Scoped styles: Styles that only apply to the specific component they're written for, without leaking out to affect other components, even if class names are reused elsewhere.

Detailed Explanation

Global CSS, the simplest approach, applies everywhere it's imported, with no automatic isolation — a class named .card in one file can silently conflict with a differently-styled .card in another file, since both ultimately compile down to the exact same global class name in the browser. This works fine for very small projects but becomes a genuine maintenance risk as a codebase grows and more people contribute styles.

CSS Modules solve this directly, using regular, familiar CSS syntax you already know. Any file named with a *.module.css extension (e.g., Card.module.css) is automatically processed by Next.js so that every class name inside it is transformed into a unique, scoped identifier at build time. When you import styles from that file into a component and use styles.card, you're referencing that unique, collision-proof class name — even if another completely unrelated component also has a class literally named .card in its own module file, the two will never conflict, because each was compiled to a distinct, unique name behind the scenes.

Tailwind CSS takes an entirely different philosophy: instead of writing custom CSS classes and rules at all, you compose your design directly in your markup using a large vocabulary of small, single-purpose utility classes — flex for display: flex, p-4 for a specific padding value, text-lg for a specific font size, and so on. Rather than switching between a JSX file and a separate CSS file, you build up a component's exact appearance right where you're already writing its structure. This tight feedback loop, combined with Tailwind's build process automatically removing any unused utility classes from the final CSS bundle, makes it extremely popular for rapid UI development, though it does mean your JSX markup tends to carry many more class names than with other approaches.

Styled Components represents a third philosophy, CSS-in-JS: you write actual CSS rules directly inside your JavaScript or TypeScript file, using a tagged template literal syntax, and the library generates a real React component with automatically scoped, unique class names applied. This colocates a component's structure and its styling in a single file, and allows genuinely dynamic styles driven directly by JavaScript logic or props (e.g., changing a button's background color based on a 'variant' prop), something plain CSS files handle less naturally. The tradeoff is a runtime cost (styles are typically generated and injected at runtime in the browser, though modern versions and alternatives increasingly support build-time extraction) and a dependency on an external library rather than native CSS support.

There's no single 'correct' choice among these three — many real projects even combine them, such as using Tailwind for most layout and utility styling while reaching for CSS Modules or Styled Components for a few highly custom, complex components. The right choice depends on team preference, project scale, and how much dynamic, prop-driven styling a given project genuinely needs.

Diagram Description

Visualize the three approaches side by side for the same 'Card' component:

CSS MODULES:
Card.module.css → .card { padding: 16px; }
Card.tsx → import styles from './Card.module.css'; <div className={styles.card}>
(compiles to a unique class like Card_card__a1B2c)

TAILWIND CSS:
Card.tsx → <div className="p-4 rounded-lg shadow-md">
(no separate CSS file; styling lives directly in the JSX)

STYLED COMPONENTS:
Card.tsx → const StyledCard = styled.div`padding: 16px;`; <StyledCard>
(CSS lives inside the JS/TS file itself, generates a scoped component)

Comparison Table

ApproachWhere Styles LiveScopingBest For
Global CSSSeparate .css files, applied site-wideNone (manual discipline required)Very small projects, base/reset styles
CSS ModulesSeparate *.module.css files, per componentAutomatic, build-time scopingTeams wanting familiar CSS with safety
Tailwind CSSInline utility classes in JSXN/A (utility classes, no custom class names)Rapid UI development, consistent design systems
Styled ComponentsInside the component's .tsx/.jsx fileAutomatic, JS-generated scopingHighly dynamic, prop-driven styling

Next.js Practical Example

// styles/Card.module.css
.card {
  padding: 16px;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

.title {
  font-size: 1.25rem;
  font-weight: 600;
}

// components/Card.tsx — using CSS Modules
import styles from './Card.module.css';

export default function Card({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <div className={styles.card}>
      <h2 className={styles.title}>{title}</h2>
      {children}
    </div>
  );
}

// The same component using Tailwind CSS instead:
export function CardTailwind({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <div className="p-4 rounded-lg shadow-sm">
      <h2 className="text-xl font-semibold">{title}</h2>
      {children}
    </div>
  );
}

In the CSS Modules version, importing styles from './Card.module.css' gives you an object whose keys (styles.card, styles.title) correspond to the class names defined in that file — Next.js automatically rewrites those class names at build time into unique identifiers, so this exact .card class name can be reused freely in a completely different module file elsewhere in the project without any conflict. The Tailwind version achieves visually equivalent styling without a separate CSS file at all, composing the exact same visual result — padding, rounded corners, a subtle shadow, and bold title text — entirely through utility classes applied directly in the JSX's className attribute.

Industry Examples

  • Large enterprise applications with many contributing teams often favor CSS Modules or Tailwind specifically for their built-in protection against accidental style collisions across a large, shared codebase.
  • Startups and small teams frequently choose Tailwind CSS for its speed of iteration, letting a small team ship polished UI quickly without maintaining a separate design system of custom CSS classes.
  • Design-system-heavy products (e.g., component libraries used across many internal apps) often use Styled Components or a similar CSS-in-JS approach to allow deeply dynamic, prop-driven theming (like dark mode or brand variants) directly tied to component logic.
  • Marketing agencies building many similar-but-different client sites often standardize on Tailwind CSS, since its utility classes make it fast to replicate consistent design patterns across many separate projects.
  • Migration projects — teams moving an older React codebase into Next.js — often keep an existing Styled Components setup initially, since it can be adopted incrementally without needing to rewrite every existing styled component immediately.

Interview Questions and Answers

Q1. What problem do CSS Modules solve compared to plain global CSS?

CSS Modules solve the problem of global class name collisions. Any class defined in a *.module.css file is automatically scoped to a unique identifier at build time, so the same class name (like .card) can be reused across many different components without any risk of one component's styles accidentally overriding another's.

Q2. What is the core philosophy behind Tailwind CSS?

Tailwind CSS follows a utility-first philosophy: instead of writing custom CSS rules and class names, you compose a component's exact appearance directly in your markup using a large vocabulary of small, single-purpose utility classes, avoiding the need to switch between separate JSX and CSS files.

Q3. What is CSS-in-JS, and how does Styled Components implement it?

CSS-in-JS is an approach where CSS rules are written directly inside JavaScript or TypeScript files rather than separate .css files. Styled Components implements this using tagged template literals to define actual CSS, generating a real React component with automatically scoped, unique class names, colocating structure and styling in one file.

Q4. What is a key advantage of Styled Components over CSS Modules for certain use cases?

Styled Components makes genuinely dynamic, prop-driven styling more natural, since styles can directly reference JavaScript variables and component props (e.g., changing a color based on a 'variant' prop) within the same file, something plain CSS files handle less directly.

Q5. Is there a single 'best' styling approach for every Next.js project?

No. The right choice depends on team preference, project scale, and how much dynamic styling is needed. Many real projects even combine approaches, such as using Tailwind for most UI while reaching for CSS Modules or Styled Components for specific, highly custom components.

MCQs With Answers

1. What file naming convention triggers Next.js's automatic CSS Modules scoping?

  1. *.css
  2. *.module.css
  3. *.scoped.css
  4. *.global.css

Answer: B. *.module.css

Explanation: Files ending in .module.css are automatically processed by Next.js so their class names are scoped to a unique identifier at build time.

2. What is the core idea behind Tailwind CSS's utility-first approach?

  1. Writing custom CSS classes for every component
  2. Composing designs using small, single-purpose classes directly in markup
  3. Generating CSS at runtime from JavaScript
  4. Using only inline style attributes

Answer: B. Composing designs using small, single-purpose classes directly in markup

Explanation: Tailwind provides a large vocabulary of small utility classes (like p-4, flex) that are combined directly in JSX rather than writing separate custom CSS rules.

3. What does CSS-in-JS, as implemented by Styled Components, mean?

  1. Writing JavaScript inside CSS files
  2. Writing actual CSS rules directly inside JavaScript/TypeScript files
  3. Using only inline styles
  4. Avoiding CSS entirely

Answer: B. Writing actual CSS rules directly inside JavaScript/TypeScript files

Explanation: Styled Components lets developers write real CSS using tagged template literals directly within their component files, generating scoped styled components.

4. What problem does CSS Modules primarily solve?

  1. Slow page load times
  2. Global class name collisions across components
  3. Missing browser support for CSS
  4. Lack of a build step

Answer: B. Global class name collisions across components

Explanation: CSS Modules automatically scope class names to be unique per component, preventing accidental style conflicts that plain global CSS is prone to.

5. Which styling approach makes dynamic, prop-driven styling most natural?

  1. Global CSS
  2. CSS Modules
  3. Styled Components
  4. None of the above support dynamic styling

Answer: C. Styled Components

Explanation: Because styles are written directly in JavaScript/TypeScript, Styled Components can easily reference component props and variables to drive dynamic style changes.

Common Mistakes to Avoid

  • Using plain global CSS for a growing project and running into unexpected style conflicts as more components and contributors are added.
  • Forgetting the .module.css file extension, which causes Next.js to treat the file as regular global CSS instead of applying automatic scoping.
  • Assuming Tailwind requires writing custom CSS classes, when its core philosophy is specifically to avoid that by composing pre-defined utility classes.
  • Mixing styling approaches inconsistently across a codebase without a clear rationale, making the project harder to maintain and onboard new developers into.
  • Underestimating Styled Components' runtime cost implications for very large applications, without evaluating whether a lighter-weight approach might better suit performance needs.

Interview Notes

  • CSS Modules use the *.module.css naming convention and are automatically scoped by Next.js at build time to prevent class name collisions.
  • Tailwind CSS is a utility-first framework where designs are composed directly in markup using small, pre-defined classes instead of custom CSS.
  • Styled Components is a CSS-in-JS library where CSS is written inside JavaScript/TypeScript files using tagged template literals, generating scoped components.
  • Each approach has different tradeoffs around familiarity, dynamic styling support, and how much CSS class name discipline is required.
  • Real projects often combine approaches rather than strictly adhering to just one.

Key Takeaways

  • Next.js supports multiple, genuinely different styling philosophies rather than forcing a single approach on every project.
  • CSS Modules offer familiar CSS syntax with automatic collision protection, a strong default for teams that prefer traditional CSS.
  • Tailwind CSS trades a more markup-heavy JSX for extremely fast, consistent UI iteration without maintaining separate CSS files.
  • Styled Components excel specifically when styling needs to be deeply dynamic and tied directly to component logic and props.

Summary

Next.js supports several distinct styling approaches, each solving the challenge of writing maintainable CSS differently. CSS Modules use familiar CSS syntax in files ending with *.module.css, which Next.js automatically scopes to unique class names at build time, preventing collisions across components. Tailwind CSS takes a utility-first approach, composing a component's exact appearance directly in markup using a large vocabulary of small, pre-defined classes rather than custom CSS rules. Styled Components implements CSS-in-JS, letting developers write actual CSS directly inside JavaScript/TypeScript files using tagged template literals, which naturally supports dynamic, prop-driven styling. There's no single correct choice among the three — the right approach depends on team preference, project scale, and how much dynamic styling a given project requires, and many real projects combine more than one approach.

Frequently Asked Questions

Global CSS applies everywhere it's imported with no automatic protection against class name collisions across files. CSS Modules (*.module.css files) are automatically scoped by Next.js at build time, so the same class name can be reused safely across different components without conflicts.

No special setup is required — simply name your CSS file with a .module.css extension (e.g., Card.module.css), then import it into your component using `import styles from './Card.module.css'` and reference class names via the returned styles object, like styles.card.

Tailwind has its own vocabulary of utility class names to learn, but many developers find it faster to work with once familiar, since you rarely need to switch context between a JSX file and a separate CSS file to make a styling change.

JSX markup tends to accumulate many utility class names, which some developers find visually cluttered compared to a single semantic class name referencing separate CSS, though tools and conventions exist to help manage this readability tradeoff.

CSS-in-JS means writing CSS rules directly inside your JavaScript/TypeScript files rather than separate CSS files. Styled Components is a popular implementation, and you might choose it specifically for its ability to easily drive styles dynamically from component props and JavaScript logic.

Yes, this is common in practice. Many projects use Tailwind for most general layout and utility styling, while using CSS Modules or Styled Components for a smaller number of highly custom or complex components.

Next.js provides first-class, built-in support for CSS Modules and global CSS without any extra setup, and has well-documented integration paths for Tailwind CSS and CSS-in-JS libraries like Styled Components, but doesn't strictly mandate any single approach.

No, both CSS Modules and Tailwind CSS produce plain CSS at build time with no runtime performance difference in how styles are applied; any performance differences between styling approaches more commonly come from CSS-in-JS libraries with runtime style generation, like traditional Styled Components.

Scoped styling means a style only applies to the specific component it was written for, without accidentally affecting other components even if they reuse the same class name. It matters because it prevents subtle, hard-to-debug visual bugs as a codebase grows and more people contribute styles.

CSS Modules are a gentle starting point since they use CSS syntax you likely already know, with automatic safety built in. Many beginners also find Tailwind CSS approachable once they get comfortable with its utility class vocabulary, and it's extremely popular in real-world Next.js projects.