CSSintermediate

Tailwind CSS vs Traditional CSS

A thorough comparison of utility-first CSS (Tailwind) versus traditional approaches including BEM, CSS Modules, and CSS-in-JS. Pros, cons, migration strategies, and when to use each.

14 min readยทPublished Apr 2, 2026
csstailwindcss-in-jsmethodology

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

  1. You add utility classes to HTML.
  2. Tailwind scans your files for class names.
  3. It generates only the CSS for classes you actually use.
  4. 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

AspectTraditional CSS / BEMTailwind CSSCSS ModulesCSS-in-JS
Styling locationSeparate .css filesHTML classesSeparate .module.cssJavaScript files
ScopeGlobal (manual)Global (utilities)Auto-scopedAuto-scoped
Naming conflictsPossible (BEM prevents)Not possibleNot possibleNot possible
Bundle sizeGrows with projectSmall (unused purged)Grows with projectRuntime overhead
Learning curveLowMediumLowMedium
CustomizationFull controlConfig-basedFull controlFull control
ResponsiveMedia queriesResponsive prefixesMedia queriesMedia queries
Dynamic stylesLimitedLimitedLimitedExcellent
IDE supportGoodGood (IntelliSense)GoodGood
Dead code removalManualAutomatic (purge)ManualAutomatic
Server renderingNo issuesNo issuesNo issuesNeeds setup
Runtime costNoneNoneNoneYes (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

MetricTailwindTraditional CSSCSS-in-JS (runtime)
CSS file size (production)Small (purged)Grows linearlyN/A (inline)
Parse timeLowMedium-HighLow
Runtime overheadNoneNoneYes
First paint impactMinimalDepends on file sizeSlightly delayed
CachingSingle CSS fileMultiple filesBundled with JS
Unused CSSAuto-removedManual cleanupAuto-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 @apply for 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.

Found this helpful?

Support devsofus โ€” help us keep creating free dev guides.

Related Articles