Skip to main content

Jun 2026 · Comparison · ~10 min read

Tailwind CSS vs CSS Modules — What I Use in Production

By Safdar Ali — frontend engineer, Bengaluru

Every new project sparks the same debate: utility classes or scoped CSS files? I ship Tailwind CSS on most Next.js marketing sites — including safdarali.in — but I still reach for CSS Modules when design tokens, animations, or designer handoff demand real stylesheets. This tailwind vs css modules comparison uses the same profile card in both approaches so you judge ergonomics, not different UIs.

Same component — Tailwind version

// components/ProfileCard.tsx
type Props = { name: string; role: string; avatarUrl: string };

export function ProfileCardTailwind({ name, role, avatarUrl }: Props) {
  return (
    <article className="flex gap-4 rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-neutral-900">
      <img
        src={avatarUrl}
        alt=""
        className="h-14 w-14 rounded-full object-cover ring-2 ring-neutral-200 dark:ring-white/20"
      />
      <div>
        <h3 className="font-semibold text-neutral-950 dark:text-white">{name}</h3>
        <p className="text-sm text-neutral-600 dark:text-neutral-400">{role}</p>
      </div>
    </article>
  );
}

No context switch — layout, colour, and dark mode sit on the JSX. Colocation is excellent when you iterate alone. Class strings get long on complex grids; I break into smaller components instead of one 200-character line.

On a Bengaluru startup landing page last quarter, the designer changed card padding three times in one sprint. With Tailwind I adjusted utilities in the same PR as copy changes — no hunting a separate CSS file. That speed is why I default to utilities for marketing. The tradeoff is readability in code review: reviewers must know common class names or rely on preview deploys.

Same component — CSS Modules version

// components/ProfileCard.module.css
.card {
  display: flex;
  gap: 1rem;
  padding: 1.25rem;
  border-radius: 1rem;
  border: 1px solid var(--border);
  background: var(--surface);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);
}
.avatar {
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 9999px;
  object-fit: cover;
}
.name { font-weight: 600; }
.role { font-size: 0.875rem; color: var(--muted); }
// components/ProfileCard.tsx
import styles from "./ProfileCard.module.css";

export function ProfileCardModules({ name, role, avatarUrl }: Props) {
  return (
    <article className={styles.card}>
      <img src={avatarUrl} alt="" className={styles.avatar} />
      <div>
        <h3 className={styles.name}>{name}</h3>
        <p className={styles.role}>{role}</p>
      </div>
    </article>
  );
}

Designers reading Figma specs often prefer this — class names map to a stylesheet they can search. Theme variables live in one CSS file instead of duplicating dark: prefixes.

CSS Modules shine when you inherit a brand system documented as SCSS or plain CSS. I once joined a project where every spacing token lived in variables — rewriting into Tailwind would have taken weeks for zero user benefit. Modules let us wrap legacy styles with scoped class names while new React components shipped.

8-criteria comparison table

CriteriaTailwind CSSCSS Modules
Colocation with JSXExcellentSplit files
Design system tokenstailwind.config themeCSS variables
Bundle sizePurged utilities (small)Only used classes ship
Complex animationsVerbose in utilitiesNatural in CSS
Onboarding juniorsLearn utility namesLearn CSS + scoping
Readable diffsLong class stringsCleaner CSS diffs
Third-party overrides@apply or arbitrary:global() when needed
Next.js default vibeVery commonBuilt-in support

When Tailwind loses

Tailwind is not weak — it is the wrong tool when the stylesheet is the product. Long-form editorial layouts, printable invoices, and white-label themes where each tenant ships different CSS files are easier to reason about in modules or global layers than in thousands of arbitrary utility strings.

Keyframe-heavy animations — staggered reveals and complex hover chains read better in a module file. Print stylesheets — Tailwind can do it; CSS is clearer. Legacy design systems already documented in SCSS variables — rewriting into utilities is waste. Highly bespoke art-directed pages where every section has unique spacing not in your token scale — fighting arbitrary values is slower than writing CSS.

/* ProfileCard.module.css — animation Tailwind would fight */
@keyframes riseIn {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}
.cardAnimated {
  animation: riseIn 0.4s ease-out both;
}

Migrating styles — before and after

// BEFORE — global CSS leaked into production
.button {
  background: blue;
}
// Every <button> on the site turned blue

// AFTER — CSS Modules scope automatically
// Button.module.css
.root { background: var(--brand); }
// Only <button className={styles.root}>

Tailwind avoids global leakage differently — utilities are atomic. Both beat unstructured global CSS from 2018 Create React App projects.

The hybrid I use on client sites

In production: Tailwind for layout, spacing, typography, responsive grids. CSS Modules (or a single globals.css) for animations, rare third-party overrides, and print rules. On a recent marketing rebuild, that split kept Lighthouse CSS payload small while designers still got a module for the hero animation — details in performance case study.

My production setup

Default stack: Next.js + TypeScript strict + Tailwind + cn() helper for conditional classes. I add modules per feature folder when a component accrues more than ~15 lines of custom CSS. At my day job, we banned new global selectors except resets.

Neither choice fixes bad component architecture — see Next.js folder structure guide for how I colocate styles with features.

I also run Prettier with the Tailwind class sorter plugin on teams that use utilities — consistent order makes long class strings diff cleanly. CSS Module projects get stylelint for nesting and variable naming instead.

The single takeaway

Tailwind for speed; CSS Modules for complexity. Pick per component, not per religion. Same card, two implementations — choose the file you will actually maintain six months from now.

Interview tip I give juniors in India: neither answer is wrong in isolation. Ask what the team already uses, whether designers pair with you daily, and whether the page is marketing or a long-lived dashboard. Match the toolchain to the delivery cadence, not Twitter polls.

Related: Projects. Contact.

If this helped you

I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:

More guides on safdarali.in — same author, production-focused.

"Talk is cheap. Show me the code."