The CSS Methodology Landscape
Writing CSS at scale is hard. As projects grow, stylesheets become tangled โ selectors conflict, specificity escalates, and dead code accumulates. Different methodologies have emerged to solve these problems, each with distinct trade-offs.
The main approaches:
Traditional CSS โ Write .css files, use class names
BEM โ Naming convention: Block__Element--Modifier
CSS Modules โ Scoped class names, auto-generated hashes
CSS-in-JS โ Styles in JavaScript (styled-components, Emotion)
Utility-First (Tailwind) โ Predefined utility classes in HTML
This article focuses on comparing Tailwind CSS with traditional approaches, helping you decide which fits your project.
What Is Tailwind CSS?
Tailwind is a utility-first CSS framework. Instead of writing custom CSS classes, you compose styles directly in HTML using small, single-purpose utility classes.
<!-- Traditional CSS -->
<button class="primary-button">Click Me</button>
<!-- Tailwind CSS -->
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Click Me
</button>
/* Traditional: you write this CSS */
.primary-button {
background-color: #3b82f6;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.primary-button:hover {
background-color: #2563eb;
}
/* Tailwind: CSS is generated from utility classes, you write nothing */
How Tailwind Works
- You add utility classes to HTML.
- Tailwind scans your files for class names.
- It generates only the CSS for classes you actually use.
- Output is a single, small CSS file.
Your HTML: class="flex items-center gap-4 p-6 bg-white rounded-lg shadow-md"
Tailwind generates:
.flex { display: flex; }
.items-center { align-items: center; }
.gap-4 { gap: 1rem; }
.p-6 { padding: 1.5rem; }
.bg-white { background-color: #fff; }
.rounded-lg { border-radius: 0.5rem; }
.shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
What Is Traditional CSS?
"Traditional CSS" refers to writing custom stylesheets with semantic class names. This includes vanilla CSS, Sass/SCSS, and methodologies like BEM.
/* Vanilla CSS */
.card {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.5rem;
background: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
/* Sass/SCSS */
.card {
display: flex;
align-items: center;
gap: $spacing-4;
padding: $spacing-6;
background: $color-white;
border-radius: $radius-lg;
box-shadow: $shadow-md;
}
BEM Convention
BEM (Block Element Modifier) is a naming convention for CSS classes:
/* Block */
.card { }
/* Element โ part of a block */
.card__header { }
.card__body { }
.card__footer { }
/* Modifier โ variation */
.card--featured { }
.card--compact { }
.card__header--large { }
<div class="card card--featured">
<div class="card__header card__header--large">Title</div>
<div class="card__body">Content</div>
<div class="card__footer">Actions</div>
</div>
Side-by-Side: Same Component
Let's build the same card component with each approach.
Traditional CSS
<div class="card">
<img class="card__image" src="photo.jpg" alt="Photo">
<div class="card__content">
<h3 class="card__title">Card Title</h3>
<p class="card__text">Card description text here.</p>
<button class="card__button">Read More</button>
</div>
</div>
.card {
display: flex;
flex-direction: column;
background: white;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.12);
overflow: hidden;
}
.card__image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card__content {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.card__title {
font-size: 1.25rem;
font-weight: 600;
color: #1a1a1a;
}
.card__text {
color: #666;
line-height: 1.5;
}
.card__button {
margin-top: auto;
padding: 0.5rem 1rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
.card__button:hover {
background: #2563eb;
}
Tailwind CSS
<div class="flex flex-col bg-white rounded-lg shadow overflow-hidden">
<img class="w-full h-48 object-cover" src="photo.jpg" alt="Photo">
<div class="p-4 flex flex-col gap-2">
<h3 class="text-xl font-semibold text-gray-900">Card Title</h3>
<p class="text-gray-500 leading-relaxed">Card description text here.</p>
<button class="mt-auto px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Read More
</button>
</div>
</div>
No separate CSS file needed. All styling is inline via utility classes.
CSS Modules
import styles from './Card.module.css';
function Card() {
return (
<div className={styles.card}>
<img className={styles.image} src="photo.jpg" alt="Photo" />
<div className={styles.content}>
<h3 className={styles.title}>Card Title</h3>
<p className={styles.text}>Card description text here.</p>
<button className={styles.button}>Read More</button>
</div>
</div>
);
}
/* Card.module.css */
.card { /* same as traditional CSS */ }
.image { /* ... */ }
.content { /* ... */ }
/* Class names are auto-hashed: .card_abc123 */
CSS-in-JS (styled-components)
import styled from 'styled-components';
const Card = styled.div`
display: flex;
flex-direction: column;
background: white;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.12);
overflow: hidden;
`;
const CardImage = styled.img`
width: 100%;
height: 200px;
object-fit: cover;
`;
const CardButton = styled.button`
margin-top: auto;
padding: 0.5rem 1rem;
background: #3b82f6;
color: white;
border-radius: 0.25rem;
cursor: pointer;
&:hover {
background: #2563eb;
}
`;
Comprehensive Comparison Table
| Aspect | Traditional CSS / BEM | Tailwind CSS | CSS Modules | CSS-in-JS |
|---|---|---|---|---|
| Styling location | Separate .css files | HTML classes | Separate .module.css | JavaScript files |
| Scope | Global (manual) | Global (utilities) | Auto-scoped | Auto-scoped |
| Naming conflicts | Possible (BEM prevents) | Not possible | Not possible | Not possible |
| Bundle size | Grows with project | Small (unused purged) | Grows with project | Runtime overhead |
| Learning curve | Low | Medium | Low | Medium |
| Customization | Full control | Config-based | Full control | Full control |
| Responsive | Media queries | Responsive prefixes | Media queries | Media queries |
| Dynamic styles | Limited | Limited | Limited | Excellent |
| IDE support | Good | Good (IntelliSense) | Good | Good |
| Dead code removal | Manual | Automatic (purge) | Manual | Automatic |
| Server rendering | No issues | No issues | No issues | Needs setup |
| Runtime cost | None | None | None | Yes (some libs) |
Tailwind: Pros and Cons
Advantages
1. Speed of development
No context switching between HTML and CSS files. Style directly where you build.
<!-- See what it does immediately -->
<div class="flex items-center justify-between p-4 border-b">
<h1 class="text-lg font-bold">Dashboard</h1>
<button class="px-3 py-1 text-sm bg-blue-500 text-white rounded">
Settings
</button>
</div>
2. No naming fatigue
You never need to invent class names. No more .card-wrapper-inner-content-header.
3. Consistent design tokens
Spacing, colors, and sizes come from a predefined scale. Everything is consistent.
<!-- Spacing scale: 1=0.25rem, 2=0.5rem, 4=1rem, 6=1.5rem, 8=2rem -->
<div class="p-4 m-2 gap-6">
<!-- p=1rem, m=0.5rem, gap=1.5rem -->
</div>
4. Small production bundles
Tailwind purges unused classes. A large app might generate only 10-30KB of CSS (gzipped).
5. Responsive design is intuitive
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 1 column on mobile, 2 on tablet, 3 on desktop -->
</div>
<p class="text-sm md:text-base lg:text-lg">
<!-- responsive font sizing -->
</p>
6. State variants built-in
<button class="bg-blue-500 hover:bg-blue-600 focus:ring-2 active:bg-blue-700
disabled:opacity-50 disabled:cursor-not-allowed">
Submit
</button>
7. Dark mode support
<div class="bg-white dark:bg-gray-900 text-black dark:text-white">
<!-- automatic dark mode -->
</div>
Disadvantages
1. Verbose HTML
<!-- This is common in Tailwind projects -->
<div class="flex items-center justify-between p-4 bg-white dark:bg-gray-800
border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm
hover:shadow-md transition-shadow duration-200">
<!-- Long class strings are hard to scan -->
</div>
2. Readability concerns
Looking at the HTML, it's not immediately clear what an element IS. Traditional CSS gives semantic names:
<!-- Traditional: clear intent -->
<nav class="main-navigation">
<!-- Tailwind: describes appearance, not purpose -->
<nav class="flex items-center justify-between px-6 py-4 bg-white shadow">
3. Design system coupling
Tailwind's utility classes are tied to its configuration. Custom values require config changes or arbitrary values.
<!-- Standard Tailwind value -->
<div class="p-4"> <!-- 1rem -->
<!-- Need 13px specifically? Arbitrary value syntax -->
<div class="p-[13px]">
<!-- Many arbitrary values = losing Tailwind's benefits -->
4. Repeated class combinations
The same set of classes gets repeated across similar elements. Tailwind's answer is @apply or component extraction.
/* @apply extracts utilities to a class (but contradicts utility-first) */
.btn-primary {
@apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}
5. Steeper team onboarding
New developers must learn Tailwind's class naming system. justify-between, tracking-wide, leading-relaxed aren't obvious without reference.
Traditional CSS: Pros and Cons
Advantages
1. Full control
No framework constraints. Write any CSS you need.
.element {
/* anything CSS supports, including newest features */
container-type: inline-size;
view-transition-name: card;
}
2. Semantic class names
HTML communicates intent, not appearance:
<div class="pricing-card pricing-card--popular">
<div class="pricing-card__header">Pro Plan</div>
</div>
3. Separation of concerns
HTML describes structure. CSS describes appearance. Changing styles doesn't require touching HTML.
/* Redesign without changing HTML */
.pricing-card {
/* old styles replaced with new styles */
/* HTML unchanged */
}
4. Lower barrier to entry
Standard CSS knowledge transfers everywhere. No framework-specific syntax.
5. Better for complex animations and layouts
Custom animations, complex selectors, and advanced features are written directly:
.card {
animation: slideIn 0.3s ease-out;
&:has(.image) { grid-template-rows: 200px 1fr; }
}
@keyframes slideIn {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
Disadvantages
1. Naming is hard
Coming up with meaningful, non-conflicting class names is a real challenge:
/* What do you name these? */
.container { } /* too generic, conflicts with frameworks */
.wrapper { } /* wrapper of what? */
.content-area { } /* vague */
.main-section { } /* still vague */
2. Global scope conflicts
Without tooling (CSS Modules, BEM), classes are global. Same name = collision.
/* page-a.css */
.title { font-size: 2rem; }
/* page-b.css */
.title { font-size: 1rem; } /* conflicts with page-a */
3. Dead code accumulation
As components are deleted, their CSS often isn't. Stylesheets grow over time.
4. Inconsistent values
Without a design system, developers pick arbitrary values:
/* Three different developers, three different spacings */
.header { padding: 15px; }
.sidebar { padding: 18px; }
.footer { padding: 12px; }
/* Tailwind enforces: p-3 (12px) or p-4 (16px) */
5. CSS bundle grows linearly
Every new component adds more CSS. Unlike Tailwind, there's no automatic purging of unused styles.
CSS-in-JS: Where It Fits
CSS-in-JS libraries (styled-components, Emotion, vanilla-extract) write styles in JavaScript.
// styled-components
const Button = styled.button`
background: ${props => props.primary ? '#3b82f6' : '#gray'};
color: white;
padding: 0.5rem 1rem;
`;
// Usage
<Button primary>Click Me</Button>
CSS-in-JS Strengths
- Dynamic styles based on props.
- Automatic scoping.
- Co-located styles with components.
- Type-safe (with TypeScript + vanilla-extract).
CSS-in-JS Weaknesses
- Runtime overhead (some libraries inject CSS at runtime).
- Larger bundle sizes.
- Server-rendering complexity.
- Not cacheable separately from JS (in runtime libs).
Zero-Runtime CSS-in-JS
Libraries like vanilla-extract and Panda CSS extract styles at build time, eliminating runtime overhead:
// vanilla-extract
import { style } from '@vanilla-extract/css';
export const button = style({
background: '#3b82f6',
color: 'white',
padding: '0.5rem 1rem',
':hover': {
background: '#2563eb',
},
});
This generates a regular CSS file at build time โ no runtime cost.
When to Use What
Use Tailwind When:
- Building a new project from scratch.
- Using a component-based framework (React, Vue, Svelte).
- Team values speed of development.
- You want consistent design tokens without building a design system.
- The project has many similar UI patterns (dashboards, admin panels, landing pages).
- You're comfortable with utility classes in HTML.
Use Traditional CSS When:
- Building a content-heavy site (blog, documentation, marketing).
- The project needs complex, custom animations.
- Team has strong CSS skills and prefers semantic HTML.
- You need to support third-party theming (users can override styles).
- Working with non-component architectures (server-rendered pages, WordPress themes).
Use CSS Modules When:
- You want scoped styles without learning a new framework.
- Working in a React/Next.js project (built-in support).
- Team prefers writing standard CSS with automatic scoping.
Use CSS-in-JS When:
- You need dynamic styles based on component props or state.
- Building a design system or component library.
- You want co-located styles with TypeScript type safety.
- Use a zero-runtime library (vanilla-extract, Panda CSS) to avoid performance issues.
Migration Strategies
From Traditional CSS to Tailwind
Step 1: Install Tailwind, keep existing CSS
Step 2: New components use Tailwind
Step 3: Gradually convert old components
Step 4: Remove old CSS files as components are converted
/* Step 3: Use @apply to convert gradually */
/* old: .card { padding: 1rem; background: white; border-radius: 0.5rem; } */
/* new: */
.card {
@apply p-4 bg-white rounded-lg;
}
/* Eventually move to pure Tailwind in HTML */
From Tailwind to Traditional CSS
Step 1: Identify repeated utility patterns
Step 2: Extract to CSS classes
Step 3: Replace utility classes in HTML
Step 4: Remove Tailwind dependency
<!-- Before: Tailwind -->
<button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
<!-- After: Traditional -->
<button class="btn btn-primary">
.btn {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
Hybrid Approach: Tailwind + Custom CSS
Many teams use Tailwind for layout and spacing but write custom CSS for complex components:
<div class="grid grid-cols-3 gap-4 p-6">
<!-- Tailwind for layout -->
<div class="custom-visualization">
<!-- Custom CSS for complex component -->
</div>
</div>
/* Custom CSS for components that don't fit utility classes */
.custom-visualization {
/* complex gradients, animations, etc. */
}
Performance Comparison
| Metric | Tailwind | Traditional CSS | CSS-in-JS (runtime) |
|---|---|---|---|
| CSS file size (production) | Small (purged) | Grows linearly | N/A (inline) |
| Parse time | Low | Medium-High | Low |
| Runtime overhead | None | None | Yes |
| First paint impact | Minimal | Depends on file size | Slightly delayed |
| Caching | Single CSS file | Multiple files | Bundled with JS |
| Unused CSS | Auto-removed | Manual cleanup | Auto-removed |
Real-world production sizes (approximate):
Tailwind (purged): ~10-30KB gzipped
Bootstrap: ~25-50KB gzipped
Large custom CSS: ~30-100KB+ gzipped
CSS-in-JS (runtime): Varies (included in JS bundle)
Real-World Observations
Large Teams
Large teams benefit from Tailwind's constraints. Everyone uses the same spacing scale, color palette, and utility classes. Code reviews become easier โ you see the styles right in the HTML diff.
Design Systems
If building a shared component library consumed by multiple projects, traditional CSS or CSS Modules give consumers more control. Tailwind's utility classes are harder to override in consuming applications.
Prototyping
Tailwind is exceptional for rapid prototyping. You can build a complete UI without writing a single CSS file or inventing a single class name.
Long-Lived Projects
Long-lived projects with traditional CSS accumulate dead code. Tailwind avoids this because styles are in the HTML โ delete the HTML, the CSS is gone automatically.
Common Arguments and Counterpoints
"Tailwind is just inline styles"
Not quite. Tailwind provides:
- Responsive prefixes (
md:,lg:) - State variants (
hover:,focus:,dark:) - Design tokens (spacing scale, color palette)
- Pseudo-elements
- Media queries
Inline styles can't do any of these.
"Tailwind makes HTML ugly"
Valid concern. Mitigation strategies:
- Extract components (React, Vue, etc.)
- Use
@applyfor frequently repeated patterns - Use IDE formatting with class sorting
"Traditional CSS has separation of concerns"
In component frameworks, the "concern" is the component itself โ markup, styles, and behavior are all part of one unit. Separating CSS into different files doesn't automatically mean better architecture.
"Tailwind can't do everything"
True. Complex animations, pseudo-element content, and advanced selectors still need custom CSS. Most Tailwind projects include some custom CSS alongside utilities.
Key Takeaways
- Tailwind is utility-first: styles in HTML via predefined classes. Fast development, consistent design, small bundles. Trade-off is verbose HTML.
- Traditional CSS offers full control and semantic class names. Better for content-heavy sites and complex custom designs. Trade-off is naming fatigue and dead code.
- CSS Modules scope styles automatically without learning a framework. Good middle ground.
- CSS-in-JS is best for dynamic, prop-based styling. Use zero-runtime libraries to avoid performance overhead.
- They're not mutually exclusive. Hybrid approaches (Tailwind for layout + custom CSS for complex components) are common and practical.
- Choose based on your team, your project, and your constraints โ not based on internet debates.
- Whatever you choose, consistency within a project matters more than which methodology you pick.