PostCSS vs Tailwind — Team Fights Explained Calmly
Half the team wants Tailwind everywhere. The other half wants “real CSS” with PostCSS nesting and custom properties. The debate is usually framed as ideology. It is really about where style decisions live and who owns the design tokens. Tailwind is a utility vocabulary compiled by PostCSS. PostCSS is a transform pipeline. Comparing them as rivals is like comparing React to Babel.
What each layer actually is
| Layer | Role |
|---|---|
| PostCSS | Runs plugins: autoprefixer, nesting, custom media, Tailwind, cssnano |
| Tailwind | Generates utility classes from a token config; itself is a PostCSS plugin |
| Hand-written CSS / CSS Modules | Local semantics, complex selectors, one-off layouts |
You can use PostCSS without Tailwind. You cannot run Tailwind’s build without a PostCSS-compatible pipeline (or Oxido/Lightning equivalents that still speak the same config ideas).
When Tailwind earns its keep
- Design system is mostly spacing, color, type scale — already encoded in
theme. - Many similar UI states (buttons, badges) where utilities beat inventing class names.
- Team ships features faster when they do not context-switch into a separate stylesheet for every margin.
- Purge/content scanning keeps CSS small if templates are discoverable.
Pain arrives when people fight the framework: 15 utilities stacked for a one-off marketing hero, arbitrary values everywhere (top-[13px]), or copying Figma literally without tokens.
When hand-written CSS (via PostCSS) wins
- Complex animations, grid areas, and selectors that utilities express poorly.
- Shadow DOM / embedded widgets where class strings are awkward.
- Teams with strong existing CSS Modules / BEM and little desire to rewrite.
- Design that is mostly unique page chrome, not repeated app chrome.
PostCSS nesting + custom properties gets you modern CSS without a utility dialect:
.card {
--pad: 1rem;
padding: var(--pad);
& h2 {
line-height: 1.3;
}
}
The fight that is really about review norms
Arguments labeled “Tailwind vs PostCSS” often mean:
- PR noise — class strings vs CSS files in diffs.
- Naming — inventing
.productCardHeaderTitlevs reading utilities. - Consistency — without lint (
eslint-plugin-tailwindcss, prettier plugin), utilities sprawl. - Hiring — juniors may know Tailwind syntax and not cascade nuance (and vice versa).
None of those are solved by banning a tool. They are solved by a written convention.
A calm policy that ends Slack wars
Default Tailwind for app UI (forms, nav, settings). CSS Modules + PostCSS for:
- Global base styles and resets
- Rich text / markdown article chrome
- One-off landing sections with bespoke layout
- Third-party widget overrides
Shared tokens live in one place — Tailwind theme or CSS variables consumed by both:
// tailwind.config — map to CSS variables
theme: {
extend: {
colors: {
brand: "rgb(var(--color-brand) / <alpha-value>)",
},
},
},
Then non-Tailwind CSS can use the same --color-brand. One palette, two access paths.
Migration without a rewrite
Do not “convert the whole repo.” New features follow the policy. Touch a file, leave it better. Ban new arbitrary values unless justified in the PR. Add a Stylelint / ESLint rule for class order so reviews stop bikeshedding.
Unpopular balanced take
Tailwind is not a substitute for understanding CSS. PostCSS is not a moral high ground. Use Tailwind as a constrained design language for repeated UI, PostCSS as the compiler and escape hatch, and document which side of the line a file is on. The team fight ends when the boundary is boring.