35% OFF
Ends14d 00:00:00
Shop
Back to Blog
tailwindcsscsswebdevfrontendnextjs

Your Tailwind CSS Isn't Messy. Your Components Are. Here's How I Fix It.

Pixel Anas··11 min read

Tailwind CSS gets blamed for huge className strings, but the real problem is usually component structure. Here are practical patterns I use to keep Tailwind projects clean without creating a giant CSS file.

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