Skip to main content

CSS Modules vs Tailwind CSS in 2026

·PkgPulse Team
0

TL;DR

Both are valid, mature approaches — choose based on team workflow preference. CSS Modules keeps styles in CSS files, close to your design vocabulary. Tailwind puts styles directly in markup, eliminating context switching. CSS Modules are great for teams that prefer traditional CSS authoring with modern scoping. Tailwind is great for rapid iteration without leaving the component file.

Key Takeaways

  • Tailwind: ~12M weekly downloads — CSS Modules: built into most frameworks, no npm package
  • CSS Modules are built into Vite, Next.js, CRA — zero configuration needed
  • Tailwind requires learning utility classes — CSS Modules uses standard CSS
  • CSS Modules support all CSS features — animations, complex selectors, variables
  • Tailwind enables consistent design system — constrained values from config

The Philosophy: Semantic Classes vs Utility-First

The fundamental disagreement between CSS Modules and Tailwind is not about syntax — it is about where meaning lives in your codebase. CSS Modules follows the semantic class philosophy: you create a .button-primary class whose name describes what the element IS, and the visual appearance is an implementation detail in the separate .module.css file. Tailwind follows the utility-first philosophy: you compose appearance directly in markup using single-purpose classes like bg-blue-600 text-white rounded-md, and the "what it looks like" is visible directly in the JSX.

Neither philosophy is objectively correct. The semantic approach aligns with how web standards evolved and how most designers think about components. The utility-first approach aligns with how developers actually work when they need to build and iterate quickly — the friction of context-switching to a CSS file, finding the right class, and coming back breaks flow.

What Tailwind's popularity reveals is that for the majority of web development work, utility-first is genuinely faster in practice. When you need to add padding-top: 8px, typing pt-2 in the JSX is faster than switching files and writing the CSS. When you need to adjust spacing for a specific breakpoint, md:pt-4 in the class is faster than writing a media query. The cumulative time savings across a day of development add up.


The Workflow Difference

CSS Modules workflow:
1. Write component JSX
2. Switch to Button.module.css
3. Write .button { ... } styles
4. Back to JSX: className={styles.button}

Tailwind workflow:
1. Write component JSX
2. Add utility classes inline: className="px-4 py-2 bg-blue-600 text-white"
3. Done — never leave the component file

Tailwind wins for speed of iteration. CSS Modules wins for separation of concerns.


Code Comparison

// CSS Modules — styles in separate file
// Button.module.css:
.button {
  display: inline-flex;
  align-items: center;
  padding: 0.5rem 1rem;
  border-radius: 0.375rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 150ms;
}

.primary {
  background-color: #2563eb;
  color: white;
}

.primary:hover {
  background-color: #1d4ed8;
}

.primary:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

// Button.tsx:
import styles from './Button.module.css';

export function Button({ variant = 'primary', disabled, children, ...props }) {
  return (
    <button
      className={`${styles.button} ${styles[variant]}`}
      disabled={disabled}
      {...props}
    >
      {children}
    </button>
  );
}
// Tailwind — everything in JSX
export function Button({ variant = 'primary', disabled, children, ...props }) {
  const variants = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    outline: 'border border-gray-300 text-gray-700 hover:bg-gray-50',
    ghost: 'text-gray-700 hover:bg-gray-100',
  };

  return (
    <button
      className={`inline-flex items-center px-4 py-2 rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed ${variants[variant]}`}
      disabled={disabled}
      {...props}
    >
      {children}
    </button>
  );
}

When CSS Modules Shine

CSS Modules are not just a legacy approach for teams resistant to change — they genuinely excel in specific scenarios. Complex animations written in CSS are more readable and maintainable in .module.css files than expressed as arbitrary Tailwind values. Multi-step @keyframes with precise timing functions, complex cubic-bezier curves, and state-dependent animation classes are all more naturally expressed in real CSS. Similarly, complex CSS selectors (:nth-child, :focus-within, sibling selectors, pseudo-elements with ::before/::after) are easier to write in CSS than as Tailwind workarounds.

/* Complex animations — natural CSS syntax */
/* Button.module.css */
@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.05); }
}

.loading {
  animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

/* Complex selectors */
.table tr:nth-child(even) {
  background-color: #f9fafb;
}

.form :focus-within {
  outline: 2px solid #2563eb;
}

/* Media queries (possible in Tailwind but verbose) */
@media (max-width: 640px) {
  .sideNav {
    position: fixed;
    inset: 0;
    transform: translateX(-100%);
    transition: transform 300ms;
  }
  .sideNav.open {
    transform: translateX(0);
  }
}

Combining Both

Many successful projects use both:

// CSS Modules for complex, component-specific styles
// Tailwind for layout and spacing
import styles from './ComplexChart.module.css';

function Chart({ data }) {
  return (
    // Tailwind for layout
    <div className="w-full h-64 p-4 bg-white rounded-lg">
      {/* CSS Module for complex SVG styles */}
      <svg className={styles.chartContainer}>
        {data.map((point, i) => (
          <circle
            key={i}
            className={`${styles.dataPoint} ${point.highlighted ? styles.highlighted : ''}`}
          />
        ))}
      </svg>
    </div>
  );
}

Package Health / Ecosystem

CSS Modules is not a package in the traditional sense — it's a specification that bundlers implement. This makes its "health" a question of bundler support rather than npm download trends. Every major JavaScript bundler and framework supports CSS Modules natively: Vite, Webpack, Next.js, Remix, and Parcel all treat .module.css files as first-class. CSS Modules support is not going anywhere; it's baked into the infrastructure.

Tailwind CSS as an npm package has one of the strongest health profiles in the ecosystem. With ~12M weekly downloads, a dedicated commercial company (Tailwind Labs) backing it, and the release of Tailwind v4 (which dramatically changed the architecture to use native CSS rather than a config file), Tailwind is actively invested in and growing. The prettier-plugin-tailwindcss package for class sorting is now standard in most Tailwind setups, solving one of the major complaints about utility class consistency.

ToolWeekly DownloadsGitHub StarsBacking
tailwindcss~12M84K+Tailwind Labs (commercial)
CSS Modules (spec)Built into bundlersWICG / Community
prettier-plugin-tailwindcss~3M5K+Tailwind Labs

Team Adoption

The team dynamics of choosing between CSS Modules and Tailwind are often the deciding factor. CSS Modules uses standard CSS — any developer who knows CSS can immediately read and write CSS Module files without learning anything new. The scoping is transparent (class names get hashed automatically, but you write them as normal names). For teams that include designers who write CSS, or for teams coming from non-React backgrounds, CSS Modules has no learning curve.

Tailwind requires learning a vocabulary of utility class names. While the classes are systematic (spacing uses a 1-4-8-16 scale, colors follow a weight system from 50 to 950), you need to internalize those names before you stop looking things up in the documentation. Experienced Tailwind developers are genuinely faster than CSS Modules developers because they never leave the component file — but there's a real ramp-up period of 2-4 weeks before most developers feel fluent.

Team style consistency is a recurring concern with Tailwind: when every developer is writing arbitrary combinations of utility classes, how do you ensure consistent patterns? The prettier-plugin-tailwindcss package enforces class ordering, which solves the "which order to write classes" problem, but does not prevent semantic inconsistencies like using py-3 px-4 in one place and p-3 in another for equivalent padding.

// .prettierrc — automatic Tailwind class sorting
{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindConfig": "./tailwind.config.ts"
}

A component library approach (like shadcn/ui) solves the consistency problem by providing pre-styled base components — developers combine library components rather than writing raw utility classes for every element.


Performance Implications

The CSS bundle size story for Tailwind vs CSS Modules is more nuanced than the marketing suggests. Tailwind v4 generates only the utility classes you actually use (via content scanning), resulting in production bundles typically between 5-20KB of CSS. CSS Modules generates CSS that's scoped to component usage — unused components produce no CSS, but each component adds CSS proportionally to its complexity.

In practice, for most applications, the CSS bundle size difference is negligible. Both approaches produce small, cache-friendly CSS bundles when used correctly. The larger concern is CSS-in-JS alternatives (like styled-components and Emotion), which serialize styles at runtime in JavaScript — this is where real performance concerns arise, and both CSS Modules and Tailwind avoid it entirely.

Critical CSS extraction — inlining the CSS needed for above-the-fold content in the <head> and deferring the rest — works with both approaches. Next.js handles this automatically for both Tailwind and CSS Modules.

CSS bundle size comparison (typical production SaaS app):

Tailwind CSS (purged): 8-15KB gzipped
CSS Modules: 12-25KB gzipped (varies by component count)
CSS-in-JS (styled-components): 20-40KB + ~15KB runtime

Both Tailwind and CSS Modules:
✓ Zero JavaScript runtime cost
✓ Browser-native CSS parsing
✓ Cacheable separately from JS bundles
✓ Support critical CSS extraction

Hybrid Approaches

Using Tailwind and CSS Modules together is not a compromise — it's frequently the right architectural choice. Use Tailwind's utility classes for layout, spacing, typography, and color work where the utility-first model is genuinely faster. Use CSS Modules for complex, stateful component styles where full CSS power is needed.

// Hybrid pattern: Tailwind for layout, CSS Modules for behavior
import styles from './Tooltip.module.css';

function Tooltip({ content, children }) {
  return (
    // Tailwind for structural positioning
    <div className="relative inline-flex items-center">
      {children}
      {/* CSS Module for the complex tooltip styles */}
      <div className={styles.tooltipContent}>
        <span className={styles.arrow} />
        {content}
      </div>
    </div>
  );
}

CSS variables for theming work excellently with both approaches — define your design tokens as CSS custom properties and reference them from either Tailwind's config or CSS Module files.

/* globals.css — design tokens as CSS variables */
:root {
  --color-primary: #2563eb;
  --color-primary-hover: #1d4ed8;
  --spacing-button: 0.5rem 1rem;
  --radius-default: 0.375rem;
}
// tailwind.config.ts — reference CSS variables
export default {
  theme: {
    extend: {
      colors: {
        primary: 'var(--color-primary)',
        'primary-hover': 'var(--color-primary-hover)',
      },
    },
  },
};

This hybrid approach means switching the entire design system's primary color is a single variable change, whether component styles use Tailwind or CSS Modules.


When to Choose

Choose Tailwind CSS when:

  • Rapid prototyping and iteration speed matter
  • Team wants to stay in the component file
  • Consistent design system with constrained values
  • Already using shadcn/ui or other Tailwind components

Choose CSS Modules when:

  • Team prefers writing actual CSS (familiar mental model)
  • Complex animations and selectors that are awkward in Tailwind
  • Strict separation of styles and markup
  • Working on projects where Tailwind's constraint feels limiting
  • Migrating from traditional CSS-in-files workflow

Compare CSS Modules and Tailwind package health on PkgPulse. For component libraries built on Tailwind, see the shadcn/ui vs Park UI vs Melt UI comparison. Explore all front-end packages in the PkgPulse directory.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.