{"id":"reyg58x0ba1fvqm","title":"Tailwind vs Vanilla CSS: Tradeoffs I Explain to Clients","slug":"tailwind-vs-vanilla-css-tradeoffs","summary":"Tailwind is not evil and vanilla is not virtuous. I pick based on team skill, cache behavior, and whether the design system is stable.","imageUrl":"https://briancrabtree.me/images/journal-tailwind-vs-vanilla-css-tradeoffs.webp","category":"CSS","date":"2026-01-19T18:00:00.000Z","featured":false,"likes":40,"author":"Brian Crabtree","content":"<h2>What Tailwind optimizes for</h2>\n\n<p>Utility-first CSS removes naming debates and ships fast prototypes. Design tokens live in tailwind.config, purge removes unused classes, and juniors stay productive without learning cascade debugging on day one. For agencies cranking landing pages, that velocity is real money.</p>\n\n<p>The cost is HTML noise and coupling between markup and design. Refactoring a spacing scale means touching hundreds of class strings unless you abstract components early in React or partials.</p>\n\n<p>Designers on Tailwind projects learn utility names instead of cascade rules. That is a training investment. Vanilla projects need a written spacing scale doc or drift appears in one-off margins.</p>\n\n<h2>What vanilla CSS optimizes for</h2>\n\n<p>Hand-authored stylesheets keep HTML semantic and readable. Custom properties plus a thin component layer give you tokens without a build step. Cache one styles.css across the site and every page benefits on repeat visits. I use that model here because journal and marketing share tokens without shipping a JS runtime for styling.</p>\n\n<p>Vanilla demands naming discipline and documentation. Without layers or BEM, you get specificity wars. With them, the CSS file becomes a durable asset that outlives whichever npm framework is trending.</p>\n\n<p>I audit Tailwind class strings for arbitrary values like w-[437px]. Those are escape hatches becoming the norm. Either allow them with comment justification or map to tokens.</p>\n\n<h2>Payload and build-time reality</h2>\n\n<p>Purged Tailwind can be tiny; misconfigured JIT with safelist bloat is not. I compare gzip sizes fairly: include fonts, critical CSS, and any runtime CSS-in-JS before declaring vanilla lighter. A 14 KB utilities file can beat a 40 KB component library if that library imported bootstrap grid globally.</p>\n\n<p>Build time matters for DX. Tailwind v4 integrated pipelines are faster than the old PostCSS stacks, but you still pay compilation on every dev save unless caching is hot.</p>\n\n<p>Critical CSS for Tailwind still exists on marketing landers. I extract above-fold utilities or accept inline critical block generated at build. LCP does not wait for full stylesheet parse.</p>\n\n<h2>Design system maturity</h2>\n\n<p>If the brand changes quarterly, utilities let you remap tokens once. If the brand is a fixed editorial system with six layouts, a single tokens.css and five component classes is simpler than scanning class strings for arbitrary values.</p>\n\n<p>I ask whether designers deliver Figma with spacing tokens or one-off comps. Tokenized comps favor Tailwind config. Bespoke art direction favors handwritten CSS with comments designers can read.</p>\n\n<p>Component libraries built on Tailwind still ship JS for headless behavior. The CSS choice does not remove interaction cost.</p>\n\n<h2>Accessibility and maintainability</h2>\n\n<p>Long class attributes make diffs noisy in review. Extract components when aria patterns repeat. Vanilla CSS keeps role and presentation separated in the markup, which some accessibility auditors prefer for readability.</p>\n\n<p>Both approaches fail when color contrast is an afterthought. I enforce contrast in design tokens regardless of toolchain.</p>\n\n<p>Long-term maintenance on vanilla CSS is grep-friendly. Long-term maintenance on utilities is ripgrep for class substrings. Pick the search story your team prefers.</p>\n\n<h2>Hybrid patterns that work</h2>\n\n<p>I have shipped Tailwind inside component libraries while marketing pages use global CSS. I have used @apply sparingly for repeated focus rings without cloning twenty class strings. Hybrids fail when two authorities fight; pick a primary system per surface.</p>\n\n<p>Migrating off Tailwind is painful because styles live in HTML. Migrating off vanilla is painful because selectors are entrenched. Choose based on team tenure, not Twitter polls.</p>\n\n<p>Some clients want Tailwind because their last vendor used it. I interview whether they want the toolchain or the velocity story. They are related but not identical.</p>\n\n<h2>My decision checklist</h2>\n\n<p>Fast campaign landers with rotating offers: Tailwind or utilities. Long-lived editorial site with strict HTML semantics: vanilla tokens. Enterprise app with hundreds of engineers: component library plus tokens, utilities optional in isolated widgets.</p>\n\n<p>If you are stuck mid-migration, I map what to freeze and what to rewrite. The goal is one coherent cascade, not purity theater.</p>\n\n<p>Hybrid tailwind plus CSS modules in one repo needs eslint boundaries per folder. Without boundaries, every PR mixes paradigms and reviewers burn out. For a related angle I keep coming back to, see <a href=\"/journal/css-custom-properties-design-tokens/\">CSS Custom Properties as Design Tokens That Survive Redesigns</a>.</p>","tags":["tailwind","vanilla-css","architecture"],"views":112}