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
| Criteria | Tailwind CSS | CSS Modules |
|---|---|---|
| Colocation with JSX | Excellent | Split files |
| Design system tokens | tailwind.config theme | CSS variables |
| Bundle size | Purged utilities (small) | Only used classes ship |
| Complex animations | Verbose in utilities | Natural in CSS |
| Onboarding juniors | Learn utility names | Learn CSS + scoping |
| Readable diffs | Long class strings | Cleaner CSS diffs |
| Third-party overrides | @apply or arbitrary | :global() when needed |
| Next.js default vibe | Very common | Built-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.
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:
- Buy me a coffee at buymeacoffee.com/safdarali
- Subscribe to my YouTube channel — it's free; 70+ React & Next.js tutorials
Related reading
More guides on safdarali.in — same author, production-focused.
- FrameSnap
Free Video Thumbnail Generator Online — No Upload
Free video thumbnail generator online — extract frames from video without upload. Browser-based, no watermark, YouTube thumbnail from MP4 workflow.
Jun 2026Read article →
- Performance
Why Your Next.js 15 App is Still Slow (And How to Fix the React 19 Hydration Lag)
Next.js 15 performance optimization — fix INP, LCP layout shifts, React 19 hydration errors, and React Compiler gaps with a production DevTools workflow.
May 2026Read article →