· tailwind / css-modules / css
Tailwind CSS vs CSS Modules — long-term costs compared
Tailwind wins year one. The long-term bill depends on one decision: design tokens or not. Here is what actually compounds over 1–3 years in a real codebase.
By Ethan
2,138 words · 11 min read
Tailwind wins year one. Fewer files, faster iteration, no naming decisions. By year two, the bill arrives — but how large it is depends almost entirely on one early decision: whether you wired Tailwind to semantic design tokens from the start, or let developers reach for bg-blue-500 directly.
The inverse is true for CSS Modules. The upfront cost is higher (two files per component, naming conventions to enforce), but the long-term maintenance path is more predictable. You always know exactly where a style lives.
Neither tool is objectively cheaper. The choice is a bet on your team’s discipline and how your codebase will grow.
Who this is for
Frontend developers choosing a styling strategy for a new production app, or teams questioning whether to migrate. This covers Tailwind CSS v4 (released January 2025) and CSS Modules as implemented in Next.js 15+. If you are evaluating CSS-in-JS alternatives like styled-components or Emotion, this comparison does not cover those. For the meta-framework question that often precedes the styling choice, see Next.js vs Astro and Next.js vs Remix.
What we compared
- Tailwind CSS v4.0 — Oxide engine (Rust-based),
@themeblocks, JIT purge. Official benchmarks from the Catalyst project via tailwindcss.com/blog/tailwindcss-v4. - CSS Modules — CSS Modules spec +
typescript-plugin-css-modulesfor IDE support +typed-css-modulesfor compile-time checking. Next.js native support, zero config. - Migration data from the auslake.vercel.app case study (CSS Modules → Tailwind, real production component library).
- Adoption data from State of CSS 2024 (9,704 respondents).
Bundle size: Tailwind wins CSS, HTML pays the difference
Tailwind v4’s JIT/purge mechanism emits only the classes that appear in your source. A typical Next.js project lands at 6–12 KB gzipped. The auslake migration showed a 97% CSS reduction: 900 KB of CSS Modules output shrank to 30 KB with Tailwind, deleting 3,000+ lines of CSS.
That number is real. It is also incomplete.
One production analysis put the other side of the ledger plainly:
“CSS went from 45 KB to 8 KB with Tailwind. Great! But HTML went from 120 KB to 340 KB. Net increase: 183 KB.”
CSS is a cacheable, route-reusable resource. A user who visits two pages on your site downloads your CSS once. HTML is not cached between pages. A Tailwind component with 15–20 utility classes per element shifts payload from the cache-friendly side of the ledger to the uncacheable side — on every page, for every user.
CSS Modules have no built-in purging, but Next.js App Router splits CSS per route automatically. Complex apps with many distinct routes can end up with more precise loading behavior than a shared Tailwind root CSS file.
Bottom line: Tailwind wins raw CSS file size convincingly. At high component density, the HTML byte cost partially offsets that win. The gap is real but smaller than the 97% figure suggests.
Build speed
Tailwind v4 official benchmarks from the Catalyst project:
| Metric | v3.4 | v4.0 | Gain |
|---|---|---|---|
| Full build | 378ms | 100ms | 3.78× faster |
| Incremental (new CSS) | 44ms | 5ms | 8.8× faster |
| Incremental (no new CSS) | 35ms | 192µs | 182× faster |
CSS Modules have no separate compile step — the bundler processes them alongside component files. For most teams the build speed difference is not the deciding factor.
Refactoring cost: where time-horizon diverges
This is where the long-term cost gap actually lives.
Tailwind: two modes
Token-based refactor (the happy path): You change colors.brand in tailwind.config.js or the @theme block in v4. Every component using bg-brand picks up the change. Zero file edits required. This is genuinely cheap.
Class-level refactor (the unhappy path): A button uses bg-blue-500 directly. You need it to be bg-indigo-600. Now you grep across every file that hardcodes that class. With hundreds of components, this is tedious and error-prone. Teams that skip the token layer and use arbitrary values (text-[#3B82F6]) pay the full refactoring tax. This is the most common source of “Tailwind is a mess in large codebases” complaints, and it is a discipline problem, not a tool problem.
Netlify’s 2021 migration of ~40 components with a 10-person team took approximately 4 weeks end-to-end. They implemented a custom utility (ctl()) to manage components with more than 5 chained classes and used visual regression testing throughout.
CSS Modules: deterministic, but naming discipline required
The co-location principle makes CSS Modules refactors predictable. import styles from './Button.module.css' makes the dependency explicit. Finding what to change requires opening one file.
The caveat: without enforced naming conventions, CSS Modules accumulate naming inconsistency — primaryButton, blueWhiteBtn, btnPrimary across a team. The auslake case study cited this exact problem as a primary migration motivator.
CSS custom properties (CSS variables) largely solve the global token problem for CSS Modules. Define --spacing-md: 16px once in a root file; reference it everywhere; update one line and every component reflects the change.
Bottom line: CSS Modules enable safer, more localized refactors when naming conventions are enforced. Tailwind enables cheaper global-token updates when semantic tokens are used from day one. Both degrade when discipline slips, in different ways.
TypeScript safety: the underwritten advantage
Most comparisons skip this. It matters in large codebases.
Tailwind IntelliSense
The official tailwindcss-intellisense VS Code extension provides autocomplete, hover documentation, and linting for invalid classes. It substantially compresses the learning curve.
What it does not do: catch typos at compile time. A misspelled text-smm produces no style and no error. TypeScript does not inspect string class names.
CSS Modules with TypeScript
Three options exist, ordered by adoption:
-
typescript-plugin-css-modules(~692K weekly npm downloads): TypeScript language service plugin — IDE autocomplete and type errors for.module.cssimports. Critical limitation: language service plugins do not run duringtsc, so CI type-checking does not catch CSS class errors. -
typed-css-modules(~186K weekly npm downloads): CLI tool that generates.d.tsdeclaration files alongside.module.cssfiles. Works withtsc. Catchesstyles.heroCradas a compile error. Cost: extra.d.tsfiles in the workspace and a generation step in your build pipeline.
When typed-css-modules is wired into CI, a misspelled class name fails the build. Tailwind cannot offer this guarantee because class names are plain strings — no type system can validate them.
Bottom line: CSS Modules with typed-css-modules catches class-name errors that Tailwind cannot. For teams where CSS regressions are real risks — large teams, rotating contributors, complex design systems — this is a meaningful long-term safety advantage.
Theming and dark mode
Tailwind
Tailwind v4’s @theme block defines tokens as both CSS custom properties and utility classes simultaneously:
@theme {
--color-brand: oklch(0.6 0.2 250);
--color-brand-dark: oklch(0.4 0.2 250);
}
This generates both bg-brand as a utility class and var(--color-brand) as a CSS variable. The gap between Tailwind and CSS Modules has narrowed significantly here.
The dark: prefix pattern (e.g., dark:bg-gray-800) works for two-theme systems but becomes verbose at scale. Sites with many per-element dark-mode variants end up with markup like:
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700 ...">
The recommended modern pattern uses semantic tokens with class-based dark mode:
@custom-variant dark (&:where(.dark, .dark *));
This eliminates most dark: proliferation, but requires upfront token architecture. Teams that skip this step and use dark: per element pay a significant refactor cost if they ever add a third theme.
CSS Modules
CSS custom properties in .module.css files decouple themes from markup entirely:
.card {
background: var(--color-surface);
color: var(--color-text-primary);
}
Define token values once in a global :root block (and a .dark override or @media (prefers-color-scheme: dark) block). Every component inherits the change without touching its markup. Adding a third theme means adding one CSS file.
Bottom line: For 2-theme (light/dark) systems, Tailwind v4’s @theme is now competitive. For multi-theme enterprise design systems with 5+ themes, CSS Modules with CSS custom properties scale more cleanly because the token layer is completely decoupled from component markup.
Team onboarding
Tailwind requires learning ~200+ utility class names and their responsive (sm:, md:) and state (hover:, focus:) prefix systems. IntelliSense makes this manageable — new developers are productive within days on common patterns. Complex responsive + dark + animation compositions take longer to master. Class soup on complex components remains a readability barrier even after full onboarding.
CSS Modules use standard CSS. Any developer who knows CSS can read a .module.css file immediately. The upfront cost shifts into naming conventions (BEM, camelCase — your choice) rather than a new syntax. This knowledge transfers to any project, which is an underrated long-term advantage when rotating junior engineers through the codebase.
State of CSS 2024 (9,704 respondents): ~75% of framework users use Tailwind; ~61% use CSS Modules. A significant subset uses both simultaneously — Tailwind for layout, spacing, and typography; CSS Modules for complex component-specific styles.
Comparison table
| Dimension | Tailwind CSS v4 | CSS Modules |
|---|---|---|
| CSS bundle size | 6–12 KB gzipped (JIT purge) | Route-split per component; no built-in purge |
| HTML byte cost | Higher (15–20+ classes per element) | Lower (single class name) |
| Full build speed | 100ms (v4 Oxide engine) | No separate compile step |
| Token change refactor | Free (if tokens in @theme) | Free (if CSS custom properties used) |
| Class rename refactor | Grep-and-replace across markup | Edit one .module.css file |
| TypeScript compile-time safety | None (strings not type-checked) | Yes, with typed-css-modules + CI |
| Dark mode (2-theme) | @theme + dark: variant — competitive | CSS variables + one override block |
| Multi-theme (5+ themes) | More complex | CSS variables — scales naturally |
| Next.js setup | PostCSS config required | Zero config, native support |
| Readability | Noisy markup; intent buried in classes | Semantic names; intent explicit |
| Files per component | 1 | 2 (.tsx + .module.css) |
| Onboarding | 200+ class names; IntelliSense helps | Standard CSS; immediately readable |
| Adoption (2024) | ~75% of framework users | ~61% of framework users |
A third option worth knowing
StyleX (Meta, open-source) is worth naming here. It is used by facebook.com and react.dev, generates atomic CSS at build time (Tailwind-like output), and expresses styles as TypeScript objects instead of string class names. A misspelled property is a TypeScript error, not a silent no-op. It solves the main weakness of both tools simultaneously: Tailwind’s lack of compile-time class safety and CSS Modules’ file proliferation. If you are starting a large design system from scratch, it deserves evaluation. No affiliate relationship — it belongs in the conversation.
Verdict
Pick Tailwind if: Your team is small (≤5 frontend engineers), you want fast iteration velocity, and you commit to semantic tokens from day one. The DX advantage is real. The maintenance cost is manageable when token discipline holds.
Pick CSS Modules if: Your team rotates contributors, you need compile-time class-name safety in CI, or you run a multi-theme design system. The higher upfront cost pays off in predictable, localized refactors.
Pick both if: You are in a Next.js app where Next.js itself recommends this — Tailwind for layout, spacing, and typography; CSS Modules for complex components where utility classes become unreadable. This is the honest default for most mid-size teams.
The most expensive decision is not which tool you pick. It is picking Tailwind and then letting developers bypass your token layer, or picking CSS Modules and then skipping naming conventions. Both tools fail the same way: by letting the easy shortcut pile up.
Related reading
- Next.js vs Astro 2026 — when to choose static sites
- React vs Svelte 2026 — DX, Bundle Size, and Ecosystem
Caveats
- Build speed benchmarks are from the official Tailwind v4 release blog (Catalyst project). Your results will vary by codebase size and machine.
- HTML bloat figures are from a dev.to case study, not our own measurement.
- Migration timelines (Netlify, auslake) reflect specific codebases and teams; your migration cost will differ.
- The TypeScript safety advantage of CSS Modules requires setup. Without
typed-css-moduleswired into CI, you get IDE hints only — not meaningfully safer than Tailwind’s IntelliSense.
References
- Tailwind CSS v4 release blog — build benchmarks, Oxide engine,
@themesyntax: tailwindcss.com/blog/tailwindcss-v4 - Tailwind dark mode docs: tailwindcss.com/docs/dark-mode
- Next.js CSS documentation — official guidance, RSC compatibility: nextjs.org/docs/app/getting-started/css
- CSS Modules GitHub specification: github.com/css-modules/css-modules
- State of CSS 2024 — tools adoption: 2024.stateofcss.com/en-US/tools
- CSS Modules → Tailwind migration case study (auslake.vercel.app) — 900 KB→30 KB, 3K lines deleted: auslake.vercel.app/blog/migration-tailwindcss
- Netlify semantic CSS → Tailwind migration: netlify.com/blog — From Semantic CSS to Tailwind
- HTML bloat analysis — CSS 45 KB→8 KB, HTML 120 KB→340 KB: dev.to — Tailwind CSS Won the War But We’re the Losers
typescript-plugin-css-modulesnpm: npmjs.com/package/typescript-plugin-css-modulestyped-css-modulesnpm: npmjs.com/package/typed-css-modules