I've seen Tailwind components that look like this:
<button
className="inline-flex items-center justify-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition-all duration-200 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
>
Save changes
</button>
And someone says:
"See? This is why I don't like Tailwind."
I actually disagree.
The problem isn't the number of classes.
The problem is that the component is doing too many things at once.
Tailwind didn't make this component difficult to maintain.
We did.
After building enough Tailwind projects, I've found that the difference between a clean Tailwind codebase and a painful one usually comes down to a few architectural decisions.
Here are the patterns I use.
1. Stop Repeating the Same 15 Classes
This is the easiest smell to spot.
You have:
<button className="rounded-lg bg-black px-4 py-2 text-sm font-medium text-white hover:bg-zinc-800">
Save
</button>
<button className="rounded-lg bg-black px-4 py-2 text-sm font-medium text-white hover:bg-zinc-800">
Continue
</button>
<button className="rounded-lg bg-black px-4 py-2 text-sm font-medium text-white hover:bg-zinc-800">
Publish
</button>
At first, this feels fine.
Then your designer says:
"Make the buttons slightly more rounded."
Now you're changing the same classes across 30 files.
That's not a Tailwind problem.
That's a component problem.
Create a reusable component:
function Button({ children, ...props }) {
return (
<button
className="rounded-lg bg-black px-4 py-2 text-sm font-medium text-white hover:bg-zinc-800"
{...props}
>
{children}
</button>
);
}
Now:
<Button>Save</Button>
<Button>Continue</Button>
<Button>Publish</Button>
One change.
Everywhere.
2. But Don't Turn Every <div> Into a Component
There's an opposite mistake.
Some developers become so worried about repetition that they create components for everything:
Container
Flex
Stack
Row
Column
Box
CardWrapper
SectionWrapper
InnerContainer
CenteredContainer
Eventually you need six files just to understand one page.
You don't need a component called:
<FlexCenterGap4>
for this:
<div className="flex items-center gap-4">
Tailwind is supposed to make simple layout code easy.
Let it.
I usually create a component when it represents a real UI concept, not simply because a className happens to be repeated.
Good:
Button
Modal
Navbar
Card
Input
Dropdown
Badge
PricingCard
Less useful:
FlexRow
CenterDiv
PaddingContainer
GapBox
The goal isn't to eliminate Tailwind classes.
The goal is to eliminate unnecessary complexity.
3. Use Variants Instead of Copy-Pasting Components
This is where Tailwind starts getting really powerful.
Imagine:
Primary button
Secondary button
Danger button
Ghost button
You could create:
PrimaryButton.tsx
SecondaryButton.tsx
DangerButton.tsx
GhostButton.tsx
But now you have four components representing the same UI concept.
Instead, define variants.
For example:
const variants = {
primary:
"bg-black text-white hover:bg-zinc-800",
secondary:
"border border-zinc-200 bg-white text-zinc-900 hover:bg-zinc-50",
danger:
"bg-red-600 text-white hover:bg-red-700",
ghost:
"text-zinc-700 hover:bg-zinc-100",
};
Then:
function Button({ variant = "primary", children }) {
return (
<button
className={`
rounded-lg px-4 py-2
text-sm font-medium
transition
${variants[variant]}
`}
>
{children}
</button>
);
}
Now:
<Button>
Save
</Button>
<Button variant="secondary">
Cancel
</Button>
<Button variant="danger">
Delete
</Button>
One component.
Multiple intentional designs.
4. Don't Make Dynamic Tailwind Classes Like This
This looks reasonable:
<div className={`bg-${color}-500`}>
Hello
</div>
But it can cause problems because Tailwind needs to detect class names in your source.
The complete class:
bg-red-500
doesn't literally exist in your source code.
You've constructed it dynamically.
Instead, map your values to complete class strings:
const colors = {
red: "bg-red-500",
blue: "bg-blue-500",
green: "bg-green-500",
};
<div className={colors[color]}>
Hello
</div>
Now Tailwind can see the actual classes.
This is one of those small rules that saves you from wondering:
"Why does this class work in development but disappear in production?"
5. Your @apply Addiction Might Be the Problem
Tailwind gives you:
@apply
And yes, it's useful.
But I've seen projects slowly turn into:
.btn {
@apply rounded-lg bg-black px-4 py-2 text-white;
}
.btn-primary {
@apply bg-blue-600;
}
.btn-large {
@apply px-6 py-3;
}
Then more.
And more.
And eventually you're maintaining a traditional CSS abstraction layer inside a Tailwind project.
I don't think @apply is bad.
I just don't use it as an escape hatch every time a className gets long.
If something is a reusable UI component, I'd rather create:
<Button />
than build a large collection of semantic CSS classes.
Use `@apply when it genuinely improves the CSS architecture—not simply because a className looks ugly.
6. Extract Design Decisions, Not Every Class
This is probably the most important Tailwind lesson I've learned.
Don't extract:
text-lg
font-medium
rounded-xl
px-4
just because you see them repeatedly.
Extract design decisions.
For example:
Button
Card
Input
Modal
Badge
Heading
Why?
Because these things have behavior.
A button isn't just:
background + padding + border-radius
It has:
hover
focus
disabled
loading
size
variant
icon
accessibility
That's a component.
That's worth an abstraction.
7. Use Arbitrary Values Carefully
Tailwind makes it incredibly easy to write:
<div className="mt-[13px]">
or:
<div className="max-w-[873px]">
or:
<div className="top-[37px]">
And sometimes that's exactly what you need.
But if your project contains:
mt-[13px]
mt-[17px]
mt-[19px]
mt-[23px]
mt-[27px]
everywhere, something is wrong.
You probably don't have a spacing system.
Instead of inventing a new value for every component, ask:
"Should this be one of our design tokens?"
A good design system should make common decisions boring.
4
8
12
16
24
32
48
64
You shouldn't need to negotiate with yourself over whether a margin should be 19px or 20px.
8. Stop Fighting Responsive Design
I see this surprisingly often:
className="
w-[347px]
sm:w-[412px]
md:w-[593px]
lg:w-[721px]
xl:w-[843px]
"
Technically valid.
But why?
If the element's width is supposed to be responsive, use a responsive layout.
For example:
<div className="w-full max-w-3xl">
Or:
<div className="mx-auto w-full max-w-5xl px-4">
Let the layout do the work.
Tailwind becomes much cleaner when you think in terms of:
container
max-width
grid
flex
gap
padding
instead of manually calculating every screen size.
9. Learn to Love gap
I've gradually stopped writing things like:
<div className="flex">
<div className="mr-4">...</div>
<div className="mr-4">...</div>
<div>...</div>
</div>
and started using:
<div className="flex gap-4">
<div>...</div>
<div>...</div>
<div>...</div>
</div>
This is more than a style preference.
gap expresses the relationship between siblings.
You're saying:
"These items have consistent spacing."
Instead of:
"Every child needs a margin."
That becomes especially useful when layouts change direction:
<div className="flex flex-col gap-4 md:flex-row">
Much easier to reason about.
10. Your Class Order Should Be Predictable
Look at this:
className="
text-white
px-4
hover:bg-black
rounded-lg
bg-blue-500
py-2
text-sm
md:px-6
"
Nothing is technically wrong.
But it's difficult to scan.
I prefer a predictable mental order:
layout
position
size
spacing
typography
colors
borders
effects
states
responsive
For example:
className="
inline-flex items-center
rounded-lg
px-4 py-2
text-sm font-medium
text-white
bg-blue-600
shadow-sm
transition
hover:bg-blue-700
focus:outline-none
md:px-6
"
The exact ordering isn't sacred.
Consistency is.
When every developer on the team follows the same structure, large Tailwind files become much easier to read.
11. Don't Build Your Entire Design System Inside className
This:
<div className="
rounded-2xl
border
border-zinc-200
bg-white
p-6
shadow-sm
dark:border-zinc-800
dark:bg-zinc-950
dark:shadow-none
">
is perfectly reasonable for a card.
But if you have 47 copies of it:
<Card>
...
</Card>
is better.
That's where I draw the line.
Long className ≠ bad code.
Repeated long className = probably a missing abstraction.
That's an important difference.
12. Use Tailwind for Styling, Not for Hiding Bad Architecture
This is probably the biggest trap.
You can make almost anything look organized with Tailwind:
<div className="flex min-h-screen items-center justify-center ...">
But Tailwind can't fix:
800-line component
200-line conditional rendering
12 useEffects
9 pieces of duplicated state
15 API requests
Sometimes the solution isn't another utility class.
It's splitting the component.
Instead of:
Dashboard.tsx
with 1,200 lines:
Dashboard
├── DashboardHeader
├── StatsGrid
├── RevenueChart
├── RecentOrders
├── ActivityFeed
└── QuickActions
Now each component can have its own Tailwind classes.
The result is easier to understand.
The Tailwind Rule I Wish More Developers Followed
Here's the rule I use:
Don't optimize Tailwind for fewer classes. Optimize it for fewer decisions.
This:
<div className="flex items-center gap-4 rounded-xl border p-4">
isn't bad because it has five utility classes.
Those five classes communicate exactly what the component is doing.
But this:
<div className="flex items-center gap-4 rounded-xl border p-4 ...">
copied across 60 files is a sign that the UI concept probably deserves a component.
The goal isn't:
"How can I make this className shorter?"
The better question is:
"How can I make this UI easier to change?"
My Tailwind Project Structure
For a larger Next.js project, I usually prefer something conceptually similar to:
components/
├── ui/
│ ├── button.tsx
│ ├── input.tsx
│ ├── card.tsx
│ ├── badge.tsx
│ └── modal.tsx
│
├── navigation/
│ ├── navbar.tsx
│ └── sidebar.tsx
│
├── dashboard/
│ ├── stats-card.tsx
│ ├── revenue-chart.tsx
│ └── recent-orders.tsx
│
└── forms/
├── login-form.tsx
└── signup-form.tsx
The exact structure isn't important.
The principle is.
Generic UI lives separately from product-specific UI.
Your Button shouldn't know anything about your dashboard.
Your DashboardStats shouldn't become the new generic component for everything.
Good boundaries make Tailwind much easier to maintain.
A Quick Test for Your Own Project
Open one of your Tailwind components.
Now ask these questions:
1. Is this component doing more than one job?
2. Have I copied this same class combination elsewhere?
3. Am I using arbitrary values everywhere?
4. Am I dynamically constructing Tailwind class names?
5. Do I have dozens of almost-identical components?
6. Could a variant replace multiple components?
7. Am I creating components that only wrap one <div>?
8. Does this UI have a real reusable concept?
If you answer "yes" to several of these, your problem probably isn't Tailwind.
It's architecture.
Tailwind Isn't Supposed to Look Like Traditional CSS
This is something I think developers fight for too long.
Tailwind isn't trying to give you:
.card {
...
}
for every visual element.
It's giving you a vocabulary for composing interfaces:
flex
grid
gap
p
m
text
font
rounded
border
shadow
bg
hover
focus
md
lg
Once you become comfortable with that vocabulary, the className stops looking like a giant pile of CSS.
It becomes a description of the UI.
And that's when Tailwind starts feeling really good.
My Tailwind Checklist Before Calling a Component "Done"
□ Is the component responsible for one clear thing?
□ Am I repeating the same UI pattern elsewhere?
□ Could a variant handle the differences?
□ Are my responsive rules simple?
□ Am I relying too heavily on arbitrary values?
□ Are dynamic classes mapped to complete class names?
□ Am I creating abstractions for real UI concepts?
□ Is the component easy to modify six months from now?
That last one matters more than whether the className is 80 characters long.
Because code isn't written only for the browser.
It's written for the developer who has to change it later.
And six months from now, that developer might be you.
Tailwind gets criticized a lot for making HTML look "too verbose."
I think that's the wrong comparison.
The real question isn't:
"Does this file have too many classes?"
It's:
"Can I understand this component and change it without being afraid of breaking five other things?"
If the answer is yes, your Tailwind code is probably doing its job.
If the answer is no, adding another utility class probably isn't going to save you.
Your component architecture might.
What's your Tailwind CSS rule that you wish every developer followed?
And be honest:
Do you prefer long utility-heavy classNames, or do you extract components as soon as you see repetition?
I'm especially interested in where people draw that line.
Get the templates: https://pixelanas.gumroad.com
*Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751