This is a debate I see constantly in React communities and I have a nuanced take on it — because both approaches have real strengths and the "Tailwind always" crowd and the "utility classes are an abomination" crowd both miss something.

What CSS Modules Actually Are

CSS Modules aren't a library. They're a build-time transformation: class names in a .module.css file are automatically scoped to the component that imports them. No global class pollution.

/* Button.module.css */
.button {
  padding: 8px 16px;
  border-radius: 6px;
  font-weight: 600;
  transition: background 0.2s;
}

.primary {
  background: #fd4766;
  color: white;
}

.primary:hover {
  background: #e03355;
}
// Button.tsx
import styles from './Button.module.css'

export function Button({ variant = 'primary', children }) {
  return (
    <button className={`${styles.button} ${styles[variant]}`}>
      {children}
    </button>
  )
}

The class names in the output HTML become something like Button_button__x7k2p — unique to this component. No conflicts possible.

What Tailwind Is

Tailwind is utility-first. You compose styles from atomic classes directly in your JSX:

export function Button({ children }) {
  return (
    <button className="px-4 py-2 rounded-md font-semibold text-white bg-[#fd4766] hover:bg-[#e03355] transition-colors">
      {children}
    </button>
  )
}

No stylesheet needed. The utility classes exist globally and you compose what you need.

The Real Tradeoffs

HTML readability. CSS Modules produce clean HTML: class="button primary". Tailwind produces long strings: class="px-4 py-2 rounded-md font-semibold text-white bg-[#fd4766] hover:bg-[#e03355] transition-colors". For simple components this is fine. For complex components with 20+ classes it becomes hard to scan.

Co-location. Tailwind wins here. Styles are right next to the markup. With CSS Modules you jump between the .tsx and .module.css files. On simple components this isn't a problem. On complex ones it's frustrating.

Consistency. Tailwind's utility scale enforces spacing consistency automatically — p-4 is always 16px, p-6 is always 24px. With CSS Modules you have to enforce your own spacing system via CSS variables or discipline.

Refactoring. Deleting a CSS Modules component leaves potentially dead CSS. Tailwind's CSS is generated from your markup — delete the component and those classes are gone from the output automatically (with PurgeCSS).

Design handoff. Designers working in Figma often use fixed values that don't match Tailwind's scale (padding: 14px when Tailwind only has 12px or 16px). CSS Modules let you write any value. Tailwind's arbitrary value syntax (p-[14px]) handles this but it's clunky.

Pseudo-elements. Tailwind can do before: and after: prefixes but complex pseudo-element work is still cleaner in plain CSS:

/* Easy in CSS Modules */
.underline-effect::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: #fd4766;
  transform: scaleX(0);
  transition: transform 0.2s ease;
}

.underline-effect:hover::after {
  transform: scaleX(1);
}

Community Momentum

Tailwind is winning the mindshare battle in 2026. shadcn/ui uses it. Most new React component libraries use it. Most Next.js tutorials use it. When you post a React question with Tailwind code, people understand it. The same question with CSS Modules gets fewer responses.

This matters practically — more examples, more copy-pasteable code, more colleagues who understand the code on sight.

My Actual Take

I use Tailwind for most new React projects — the ecosystem alignment with shadcn/ui and Next.js is too convenient to ignore and I've gotten fast enough with utility classes that the productivity is real.

I reach for CSS Modules when:

  • Building a design system that will be consumed by other teams
  • Working with a designer who provides pixel-specific values that don't fit Tailwind's scale
  • The component has complex pseudo-element animations
  • The codebase was already using CSS Modules and consistency matters more than my preference

Neither is wrong. The "Tailwind is always right" camp and the "utility classes are unmaintainable" camp are both too dogmatic. Use the approach that fits your team, your codebase and your project requirements.

Frequently Asked Questions

Neither is universally better — they have different tradeoffs. Tailwind is faster for building UIs when you know the utility classes. CSS Modules produce cleaner HTML and are easier to read for developers who think in CSS. Tailwind has larger community momentum in 2026.
Yes. Some teams use Tailwind for layout and spacing utilities, CSS Modules for complex component-specific styles. The combination works but adds cognitive overhead of switching between two systems.
Yes. Next.js supports CSS Modules out of the box. Name your file Component.module.css and import styles from it. Next.js automatically scopes the class names.
CSS Modules paired with CSS custom properties gives you better encapsulation for a design system. Tailwind works well for utility-heavy component libraries. Most major design systems (like shadcn/ui) use Tailwind because it's composable from the consumer side.

Related Comparisons

Already Decided on Bootstrap?

Get a complete Angular 21 + Bootstrap 5 admin dashboard template — production ready.

Browse Templates →

Use code FIRST30 for 30% off.