Tailwind CSS reached over 50 million weekly npm downloads by 2025, surpassing Bootstrap as the most downloaded CSS framework. The primary reason is developer velocity — writing utility classes directly in JSX eliminates the context switching of creating a separate .module.css file, naming a class, importing it, and applying it. Across a component-heavy React app, that friction compounds significantly.
The Oxide engine, written in Rust, makes full builds up to 5x faster than Tailwind v3 and incremental builds over 100x faster. At runtime, Tailwind only generates the CSS classes actually used in your project via content scanning, so a typical portfolio or SaaS app ships just 10–30KB of CSS. CSS Modules also produce minimal CSS, meaning runtime performance between the two approaches is essentially identical.
The cn() utility combines clsx and tailwind-merge to handle conditional class names cleanly. Without it, dynamic Tailwind classes result in messy ternary-filled className strings that become hard to read. A call like cn('rounded-xl p-4', isActive && 'bg-blue-500', disabled && 'opacity-50') keeps conditional logic legible, and the author credits this single pattern with eliminating 90% of Tailwind readability complaints.
CSS Modules are the better choice in three specific scenarios: publishing a component library to npm (you cannot require consumers to install Tailwind), working on teams that include dedicated designers who write CSS directly, and building projects that require complex animations or pseudo-elements needing full CSS power. For Next.js projects where the entire team consists of developers and delivery speed matters, Tailwind is almost always preferable.
Mixing both approaches in the same project creates ordering conflicts — Tailwind's reset and base styles can override, or be overridden by, your module CSS depending on non-deterministic bundler import order. The recommended approach is to pick one per project and stick with it. The one documented exception is using CSS Modules solely for global @keyframe animations while keeping all component styling in Tailwind.
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.
| Dimension | Tailwind CSS | CSS Modules |
|---|---|---|
| Popularity | 50M+ weekly npm downloads, most downloaded CSS framework | Not a standalone package, bundled into css-loader (~19M weekly) |
| Build performance | v4 Oxide engine (Rust): up to 5x faster full builds, 100x+ incremental | Built into webpack css-loader, no separate build step |
| Developer experience | Utility classes in JSX, fast iteration, no naming decisions | Clean HTML, scoped class names, no global collisions |
| Runtime bundle size | Typically 10-30KB CSS via content scanning | Also minimal, only used classes extracted, near-identical output |
| Publishing to npm | Forces consumers to install Tailwind, awkward for libraries | No extra dependency, safe choice for published packages |
| Designer collaboration | Best for developer-only teams comfortable with utility classes | Respects a designer mental model who writes CSS directly |
| Best fit | New Next.js apps, developer-centric teams, paired with shadcn/ui | Component libraries, complex animations, mixed designer-dev teams |
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.