"Wait, Is This Just Inline Styles?"
That's exactly what I thought when I first saw Tailwind. A senior developer showed me some code:
<div class="flex items-center justify-between px-6 py-4 bg-white rounded-lg shadow-md">
<h2 class="text-2xl font-bold text-gray-800">Title</h2>
<button class="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600">
Click
</button>
</div>
"What the hell... Why is the HTML so messy?" I'm not a CS major, not a designer either, but even I could tell this looked wrong. Isn't this just inline styles with fancy names?
My senior colleague laughed and said, "Just try it. You won't go back."
I didn't understand at the time. Why would anyone abandon perfectly good CSS for this?
The Pain of Traditional CSS
When I first learned web development, "Separation of Concerns" was gospel. HTML for structure, CSS for styling, JavaScript for behavior. Clean separation was the right way, they said.
So I wrote code like this:
<!-- HTML -->
<div class="card">
<h2 class="card__title">Title</h2>
<button class="card__button">Click</button>
</div>
/* CSS */
.card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.card__title {
font-size: 1.5rem;
font-weight: bold;
color: #1f2937;
}
.card__button {
padding: 0.5rem 1rem;
color: white;
background-color: #3b82f6;
border-radius: 0.25rem;
}
.card__button:hover {
background-color: #2563eb;
}
BEM naming conventions, separate CSS files. Looks clean at first glance.
But as the project grew, problems emerged:
1. Naming Hell
.card-container, .card-wrapper, .card-inner, .card-content, .card-header-section... What's the difference? Even I don't know. When I look back at my code, I think "Why did I name it this way?"
2. File Jumping
To change one button color: check the class name in HTML, open the CSS file, find the rule, modify it, go back to HTML to verify... It's like reading two books simultaneously.
3. CSS Files Growing Infinitely
Every new component adds more CSS. I try to reuse existing styles, but they're always "similar but slightly different," so I create new ones. Eventually, the CSS file exceeds 3000 lines, and nobody knows what's used where.
4. Same Styles, Different Names
.margin-top-small { margin-top: 8px; }
.mt-2 { margin-top: 8px; }
.header-spacing { margin-top: 8px; }
Same style defined three times. Someone used .margin-top-small, another person created .mt-2, and yet another made .header-spacing.
This was the trap of "Separation of Concerns." Files were separated, but the developer's mind wasn't. You still had to think about CSS and HTML simultaneously.
"Aha, Now I Get Why This Exists"
About three months into the project, our CSS file became so complex we decided to refactor. My senior suggested, "Let's just try Tailwind."
Initially, I resisted hard. The class names were so long, making HTML look messy.
But after one day of use, I was shocked.
I didn't create a single CSS file, yet all styling was complete.
I finished the design while looking only at HTML. No file jumping. No agonizing over class names. Just write flex, items-center, px-4.
It felt like assembling LEGO blocks. Combining pre-made pieces to create the shape I wanted.
That's when I understood. Tailwind combined "the convenience of inline styles" with "the reusability of CSS."
What Is Utility-First?
Tailwind's core is the Utility-First philosophy. Instead of meaningful names, it provides function-based classes.
Traditional CSS thinks:
- "This is a card, so let's create a
.cardclass" - "This is a button, so let's create a
.buttonclass"
Tailwind thinks:
- "Need flex? Use
.flex" - "Need padding? Use
.p-4" - "Need blue background? Use
.bg-blue-500"
Like atoms combining to form molecules.
Responsive Design Is Too Easy
Responsive design in traditional CSS:
.grid-container {
display: block;
}
@media (min-width: 640px) {
.grid-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.grid-container {
grid-template-columns: repeat(3, 1fr);
}
}
In Tailwind:
<div class="block sm:grid sm:grid-cols-2 lg:grid-cols-3">
<!-- content -->
</div>
One line. No need to write media queries directly. Just prefix with sm:, md:, lg:.
Initially, I thought "That's a lot to memorize," but patterns emerged:
- Default (mobile): no prefix
- 640px+:
sm: - 768px+:
md: - 1024px+:
lg: - 1280px+:
xl:
No more memorizing media query numbers.
Dark Mode in One Line
Typical dark mode implementation:
.card {
background-color: white;
color: black;
}
@media (prefers-color-scheme: dark) {
.card {
background-color: #1f2937;
color: white;
}
}
In Tailwind:
<div class="bg-white dark:bg-gray-800 text-black dark:text-white">
<!-- content -->
</div>
Just prefix with dark:. It automatically detects system dark mode.
For manual toggle, in tailwind.config.js:
module.exports = {
darkMode: 'class', // use 'class' instead of 'media'
// ...
}
Then dark mode applies only when <html class="dark"> is present.
Real Example: Responsive Card Component
Code beats theory. Let's build a responsive card component.
Requirements:
- Mobile: vertical layout, full width
- Tablet: 2-column grid
- Desktop: 3-column grid
- Dark mode support
- Hover effects
Traditional CSS:
<div class="card-grid">
<div class="card">
<img src="thumbnail.jpg" class="card-image" />
<h3 class="card-title">Title</h3>
<p class="card-description">Description</p>
</div>
</div>
.card-grid {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.card-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.card-grid { grid-template-columns: repeat(3, 1fr); }
}
.card {
background: white;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.card-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-title {
padding: 1rem;
font-size: 1.25rem;
font-weight: 600;
}
.card-description {
padding: 0 1rem 1rem;
color: #6b7280;
}
@media (prefers-color-scheme: dark) {
.card {
background: #1f2937;
}
.card-title {
color: white;
}
.card-description {
color: #9ca3af;
}
}
Tailwind:
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-md hover:shadow-lg hover:-translate-y-1 transition-all">
<img src="thumbnail.jpg" class="w-full h-48 object-cover" />
<h3 class="px-4 pt-4 text-xl font-semibold text-gray-900 dark:text-white">
Title
</h3>
<p class="px-4 pb-4 text-gray-600 dark:text-gray-300">
Description
</p>
</div>
</div>
30 lines of CSS vs self-contained HTML. Which is faster?
I found Tailwind much faster. No file jumping—just work while looking at HTML.
But It's So Long - Component Extraction
When classes get too long, extract components.
React example:
function Card({ title, description, image }) {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-md hover:shadow-lg hover:-translate-y-1 transition-all">
<img src={image} className="w-full h-48 object-cover" />
<h3 className="px-4 pt-4 text-xl font-semibold text-gray-900 dark:text-white">
{title}
</h3>
<p className="px-4 pb-4 text-gray-600 dark:text-gray-300">
{description}
</p>
</div>
);
}
// Usage
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Card title="Title 1" description="Desc 1" image="img1.jpg" />
<Card title="Title 2" description="Desc 2" image="img2.jpg" />
</div>
Extract repetitive long classes into components. Reusable and clean HTML.
Extract with @apply
When creating components feels awkward, use @apply:
/* styles.css */
@layer components {
.btn-primary {
@apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors;
}
}
<button class="btn-primary">Click</button>
But Tailwind's official docs recommend minimal @apply use. Because it abandons Tailwind's advantages.
I only use it when truly necessary. When repeating the same style 10+ times.
JIT Mode - Just-In-Time
Old Tailwind pre-generated all utility classes. Development CSS files grew to 3-4MB.
Since Tailwind 3.0, JIT (Just-In-Time) mode is default. It generates only needed classes on-demand.
<!-- When you write this -->
<div class="top-[117px]">
Tailwind instantly generates .top-\[117px\] { top: 117px; }.
Previously, only predefined values (top-0, top-1, top-2) were available. Now arbitrary values work:
<div class="bg-[#1da1f2]"> <!-- Twitter blue -->
<div class="w-[347px]"> <!-- Exact width from designer -->
<div class="grid-cols-[1fr_2fr_1fr]"> <!-- Custom grid -->
This was a game-changer. When designers say "This color must be exactly #1da1f2," I can use it immediately without config changes.
tailwind.config.js - Customization
Every project has different design systems. Tailwind extends via config:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: '#5b21b6',
secondary: '#ec4899',
},
spacing: {
'128': '32rem',
'144': '36rem',
},
fontFamily: {
sans: ['Pretendard', 'sans-serif'],
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}
Then use like this:
<div class="text-primary bg-secondary h-128 font-sans">
Define company design system in config, and team members use consistent styles. Forces prevention of weird values like "margin: 13px."
PurgeCSS - Production Optimization
During development, all utility classes are generated. But production needs only actually used classes.
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./public/index.html',
],
// ...
}
During build, Tailwind scans these files and includes only used classes in final CSS.
Result: Development 3MB → Production 10KB
That's Tailwind's magic. Unlimited utilities, but tiny final bundle.
Tailwind vs CSS-in-JS (styled-components)
Team debate: "Tailwind vs styled-components, which is better?"
styled-components camp:
- "Great TypeScript autocomplete"
- "Easy props-based dynamic styling"
- "Real CSS syntax as-is"
Tailwind camp:
- "All styles generated at build time, no runtime overhead"
- "No class name collision worries"
- "Design system enforcement for consistency"
I've used both, and it depends on project nature:
- Prototypes, startups, rapid development: Tailwind
- Complex theme systems, lots of dynamic styling: styled-components
But nowadays, most choose Tailwind. Better performance, smaller bundle, lower learning curve.
Plugin System
Tailwind extends via plugins:
Official plugins:
@tailwindcss/forms: Form style reset@tailwindcss/typography: Markdown prose styling@tailwindcss/aspect-ratio: Aspect ratio utilities@tailwindcss/line-clamp: Text line limiting
Usage:
// tailwind.config.js
module.exports = {
plugins: [
require('@tailwindcss/typography'),
],
}
<!-- Apply to markdown content -->
<article class="prose lg:prose-xl dark:prose-invert">
<h1>Title</h1>
<p>Content...</p>
</article>
One prose class creates beautiful typography.
Closing Thoughts
When I first saw Tailwind, I thought "What is this, how is it different from inline styles?"
But after using it, I understood. This isn't just writing CSS in HTML—it's a method to maximize productivity while enforcing design systems.
Freedom from naming hell, no more file jumping, consistent styling across team members.
Sure, there are downsides. HTML looks messy, memorizing class names is hard initially.
But once you're comfortable, there's no going back. Like when first using VS Code. Complex settings at first, but once familiar, can't use other editors.
Tailwind is now my default stack. The first library I install when starting new projects.
It came down to this: "Developer Experience" matters more than "Separation of Concerns."