Tailwind CSS now pulls over 50 million weekly npm downloads — 12.5x more than Bootstrap — while CSS Modules remain the built-in styling solution for Next.js with no extra dependency. I've used both on production projects and on my own portfolio (matthewswong.com), which runs 100% on Tailwind. This isn't a debate between two equal choices — for most modern web apps, the verdict is clear. But CSS Modules still has a real home.
Tailwind CSS v4 (January 2025) was a landmark release — full builds are up to 5x faster than v3, incremental builds over 100x faster via the new Oxide engine written in Rust. The library crossed 50 million weekly npm downloads and surpassed Bootstrap as the most downloaded CSS framework. Meanwhile, CSS Modules isn't a single npm package — it's built into webpack's css-loader and processed automatically by Next.js. css-loader sees ~19 million weekly downloads, but that's bundled usage, not standalone adoption.
The reason Tailwind won is developer velocity, not performance. Writing border border-gray-200 rounded-xl p-4 directly in JSX is faster than switching to a .module.css file, writing a class name, importing it, and applying it. For a component-heavy React app, this friction compounds across hundreds of components. CSS Modules trade some velocity for separation of concerns — your HTML stays clean, styles are colocated per component file, and you never worry about class name collisions globally.
┌────────────────────────────┬────────────────────────────┐
│ Tailwind CSS │ CSS Modules │
├────────────────────────────┼────────────────────────────┤
│ 50M+ weekly npm downloads │ Built into css-loader │
│ v4: Oxide engine (Rust) │ No extra dependency │
│ Utility classes in JSX │ Scoped class names │
│ PurgeCSS built-in │ Explicit class creation │
│ shadcn/ui ecosystem │ Designer-friendly │
├────────────────────────────┼────────────────────────────┤
│ Button.tsx │ Button.tsx + Button.module │
│ className="px-4 py-2 │ import styles from ... │
│ bg-blue-500 text-white │ className={styles.button} │
│ rounded-lg hover:..." │ │
├────────────────────────────┼────────────────────────────┤
│ ✅ Fast iteration │ ✅ Clean HTML │
│ ✅ No naming decisions │ ✅ CSS-native power │
│ ✅ Design system built-in │ ✅ No class conflicts │
│ ❌ Verbose in JSX │ ❌ File context switching │
│ ❌ Learning curve │ ❌ More boilerplate │
└────────────────────────────┴────────────────────────────┘From building matthewswong.com entirely with Tailwind: use the cn() utility (from clsx + tailwind-merge) for conditional classes. Without it, you end up with ternary-filled className strings that become unreadable. With it, dynamic class logic reads cleanly: cn('rounded-xl p-4', isActive && 'bg-blue-500', disabled && 'opacity-50'). This single pattern eliminates 90% of Tailwind readability complaints.
Tailwind v4's performance story is strong — the Oxide engine dramatically reduced build times. For runtime, Tailwind generates only the CSS classes you actually use (via content scanning), so production bundles are small. A typical portfolio or SaaS app ships 10-30KB of CSS with Tailwind. CSS Modules also produce minimal CSS — only the classes in used modules get extracted. The runtime performance of both approaches is essentially identical since both generate standard CSS classes that browsers handle natively.
// Install the cn utility
npm install clsx tailwind-merge
// lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Usage in components
import { cn } from "@/lib/utils"
function Button({ variant, disabled, className }: ButtonProps) {
return (
<button
className={cn(
"px-4 py-2 rounded-lg font-medium transition-colors",
variant === "primary" && "bg-blue-500 text-white hover:bg-blue-600",
variant === "ghost" && "bg-transparent hover:bg-slate-100",
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
className // allow consumer overrides
)}
>
{children}
</button>
)
}
// Tailwind v4: New CSS-first config (no more tailwind.config.js required)
// In your CSS file:
@import "tailwindcss";
@theme {
--color-brand: #3b82f6;
--font-sans: "Inter", sans-serif;
}CSS Modules shine in three scenarios: (1) Component libraries you publish to npm — you can't require consumers to install Tailwind. (2) Teams with dedicated designers who write CSS — CSS Modules respect the designer's mental model better than utility classes. (3) Projects with complex animations or pseudo-elements that need full CSS power without arbitrary value gymnastics. For Next.js projects where the team is all developers and velocity matters, Tailwind is almost always the better call.
If you try to mix Tailwind and CSS Modules in the same project, you'll hit ordering conflicts. Tailwind resets and base styles can override or be overridden by your module CSS depending on import order, which is non-deterministic with bundlers. Pick one approach per project and stick to it. The one exception I allow: use CSS Modules for global @keyframe animations (since Tailwind arbitrary animations get verbose), but keep all component styling in Tailwind.
I use Tailwind CSS on every new project, without exception. The velocity gain is real and the DX improvement is significant — I prototype a new component in half the time compared to CSS Modules. On matthewswong.com, I combined Tailwind with Framer Motion for animations and shadcn/ui for accessible component primitives. The whole setup took one day to configure and never required touching a CSS file after that. For a solo developer building production web apps, that's the winning stack.
Migrating an existing CSS Modules project to Tailwind is a non-trivial but manageable task. The approach I recommend: install Tailwind alongside CSS Modules (they can coexist temporarily), convert one component at a time starting from the smallest leaf components, and delete the module file once the component is converted. Use the Tailwind CSS IntelliSense VSCode extension from day one — it turns class name guessing into autocomplete. A 50-component app takes roughly a week for one developer to migrate.
Tailwind CSS for new projects with a developer-centric team. CSS Modules for published component libraries, designer-developer mixed teams, or when you need full CSS control without constraints. Both generate near-identical runtime performance — the choice is about developer experience and team workflow, not technical capability. If you're starting a Next.js app in 2025, use Tailwind with shadcn/ui. You'll thank yourself at 50 components.