Skip to main content

Command Palette

Search for a command to run...

Building eslint-plugin-tailwind-group and prettier-plugin-tailwind-group: Solving Tailwind CSS Readability at Scale

How a viral LinkedIn post about Tailwind's "messy" code inspired me to build a tool that automatically organizes utility classes into semantic groups

Published
8 min read
Building eslint-plugin-tailwind-group and prettier-plugin-tailwind-group: Solving Tailwind CSS Readability at Scale

The Inspiration: A Simple Observation That Resonated

Every developer who has worked with Tailwind CSS knows the feeling. You open a component file, and you’re greeted with something like this:

<select className="border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed" />

Your eyes glaze over. You scroll horizontally. You try to find that one padding value you need to change. It's in there somewhere...

This frustration was articulated perfectly by Yangshun Tay, the Ex-Meta Staff Engineer, creator of Docusaurus 2, author of the famous "Blind 75" interview questions, and founder of GreatFrontEnd. In a LinkedIn post that sparked widespread discussion in the frontend community, Yangshun highlighted what many of us had been thinking: Tailwind CSS, for all its power, can produce some genuinely messy and unreadable code.

That post was the catalyst I needed. I had been thinking about this problem for a while, and seeing it articulated by someone with Yangshun's experience and reach gave me the push to actually build a solution.

The Problem: Why Tailwind Gets Messy

Tailwind CSS is a utility-first framework, which means styling is done by composing small, single-purpose classes directly in your markup. This approach has significant advantages: co-location of styles with structure, no context-switching to CSS files, and a consistent design system.

But it comes with a trade-off: class strings grow quickly and become difficult to scan.

Consider a real-world component. You might need classes for:

  • Sizing: h-9, w-full, min-w-0

  • Layout: flex, items-center, justify-between

  • Spacing: px-3, py-2, mt-4

  • Borders: border, rounded-md, outline-none

  • Colors: bg-white, text-gray-900, placeholder:text-muted

  • States: hover:shadow-md, focus:ring-2, disabled:opacity-50

  • Responsive: sm:flex-row, md:px-6, lg:text-lg

When all of these classes are concatenated into a single string with no logical organization, cognitive load skyrockets. Developers spend more time parsing class lists than writing actual code.

The Solution: Semantic Grouping with Inline Comments

What if your class strings could look like this instead?

<select
  className={clsx(
    // Size
    "h-9 w-full min-w-0",
    // Layout
    "relative",
    // Spacing
    "px-3 py-2 pr-9",
    // Border
    "border border-input outline-none rounded-md",
    // Background
    "bg-transparent dark:bg-input/30 dark:hover:bg-input/50 selection:bg-primary",
    // Text
    "text-sm selection:text-primary-foreground placeholder:text-muted-foreground",
    // Effects
    "appearance-none shadow-xs transition-[color,box-shadow]",
    // Others
    "disabled:pointer-events-none disabled:cursor-not-allowed"
  )}
/>

Same classes. Same output. Dramatically better readability.

This approach organizes classes into semantic categories, each on its own line with an inline comment explaining what that group controls. It's like having a mini style guide embedded directly in your component.

Introducing Tailwind Class Grouper

I built Tailwind Class Grouper to automate this transformation. It's available as both an ESLint plugin and a Prettier plugin, so it integrates seamlessly into existing workflows regardless of your team's tooling preferences.

Links

How It Works: The Classification System

The plugins analyze each Tailwind class and categorize it into one of nine semantic groups based on pattern matching:

  1. Size

Width, height and text size utilites

w-*, h-*, min-w-*, max-w-*, min-h-*, max-h-*, size-*, text-xs, text-sm, text-base, text-lg, text-xl, text-2xl...

  1. Layout

Display, position and flex/grid alignment properties.

flex, grid, block, inline, absolute, relative, fixed, sticky, justify-*, items-*, content-*, place-*, float-*, clear-*

3. Spacing

Margin, padding, and gap utilities.

m-*, p-*, mx-*, my-*, mt-*, mr-*, mb-*, ml-*, px-*, py-*, pt-*, pr-*, pb-*, pl-*, gap-*, space-*

4. Border

Border, ring, outline, and border-radius utilities.

border, border-*, ring-*, outline-*, rounded-*, divide-*5

  1. Background

Background colors, gradients, and backdrop effects.

bg-*, from-*, via-*, to-*, backdrop-*

6. Text

Typography, font properties, and text decoration.

text-* (colors), font-*, tracking-*, leading-*, uppercase, lowercase, capitalize, truncate, line-clamp-*, placeholder:*

7. Effects
Shadows, opacity, transforms, transitions, and animations.

shadow-*, opacity-*, transform, scale-*, rotate-*, translate-*, skew-*, transition-*, duration-*, ease-*, animate-*

8. States & Variants

Pseudo-classes and responsive breakpoints.

hover:*, focus:*, active:*, disabled:*, dark:*, sm:*, md:*, lg:*, xl:*, 2xl:*

9. Others

Any utilities that don't match the above patterns fall into this catch-all category.

Getting Started

Installation

Choose the plugin that matches your workflow:

ESLint Plugin:

npm install --save-dev eslint-plugin-tailwind-group
# or
yarn add -D eslint-plugin-tailwind-group
# or
pnpm add -D eslint-plugin-tailwind-group

Prettier Plugin:

npm install --save-dev prettier-plugin-tailwind-group
# or
yarn add -D prettier-plugin-tailwind-group
# or
pnpm add -D prettier-plugin-tailwind-group

Note: The Prettier plugin requires Prettier v3 and assumes clsx (or a compatible helper like classnames) is available in your project, as the grouped output is wrapped in clsx(...).

Configuration

ESLint Configuration (.eslintrc.js):

module.exports = {
  plugins: ['tailwind-group'],
  rules: {
    'tailwind-group/group-tailwind-classes': [
      'warn',
      {
        formatInline: false,  // Set to true to format even small class lists
        useClsx: true         // Set to false if you don't use clsx
      }
    ]
  }
};

Prettier Configuration (prettier.config.js):

module.exports = {
  plugins: ['prettier-plugin-tailwind-group'],
  tailwindGroup: true,              // Enable the plugin
  tailwindGroupMinClasses: 4,       // Minimum classes before grouping kicks in
  tailwindGroupIncludeComments: true // Include inline category comments
};

VS Code Integration

For automatic formatting on save:

With ESLint:

{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}

With Prettier;

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}

Working with Existing Tooling

Compatibility with prettier-plugin-tailwindcss

If you're already using the official prettier-plugin-tailwindcss for class sorting, you can use both plugins together. Configure the order so Tailwind sorts first, then our grouper organizes:

module.exports = {
  plugins: [
    'prettier-plugin-tailwindcss',   // Sort first
    'prettier-plugin-tailwind-group' // Then group
  ]
};

Working with clsx/classnames

The plugins handle existing clsx or classnames usage gracefully:

Input:

<div 
  className={clsx(
    "mt-4 flex items-center justify-between px-6 py-3 bg-white border rounded-lg shadow-sm hover:shadow-md", 
    someCondition && "opacity-50"
  )} 
/>

Output:

<div
  className={clsx(
    // Layout
    "flex items-center justify-between",
    // Spacing
    "mt-4 px-6 py-3",
    // Border
    "border rounded-lg",
    // Background
    "bg-white",
    // Effects
    "shadow-sm hover:shadow-md",
    someCondition && "opacity-50"
  )}
/>

Notice how conditional classs are preserved and moved to the end of the grouped output.

Tailwind CSS IntelliSense
The grouped format maintains full compatibility with the Tailwind CSS IntelliSense VS Code extension. You stil get autocomplete suggestins, hover previews, and linting within the grouped strings.

CLI Usage

You can also run the plugins via command line for batch processing:

ESLint:

# Check files
npx eslint "src/**/*.{js,jsx,ts,tsx}"

# Auto-fix files
npx eslint "src/**/*.{js,jsx,ts,tsx}" --fix

Prettier:

# Check files
npx prettier --check "src/**/*.{js,jsx,ts,tsx}"

# Format files
npx prettier --write "src/**/*.{js,jsx,ts,tsx}"

Package.json Scripts:

{
  "scripts": {
    "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
    "lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
    "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
    "format:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx}'"
  }
}

Design Decisions and Trade-offs

Why clsx?

I chose to output grouped classes wrapped in clsx() for several reasons:

  1. It's already ubiquitous: Most React projects using Tailwind already have clsx or classnames installed for conditional class handling.

  2. Template literal alternatives are messier: While you could theoretically use template literals, they don't handle conditional classes as elegantly and often result in extra whitespace issues.

  3. It's explicit: Each group is clearly separated, making the logical structure visible at a glance.

Why inline comments?

The inline comments (// Size, // Layout, etc.) serve as documentation within the code. They can be disabled via configuration (tailwindGroupIncludeComments: false) for teams that prefer a cleaner look, but I've found they significantly improve readability, especially for developers new to a codebase.

The minimum classes threshold

By default, the Prettier plugin only groups when there are 4+ classes. This prevents over-engineering simple elements:

// This stays as-is (only 2 classes)
<div className="flex gap-4">

// This gets grouped (5+ classes)
<div className={clsx(
  // Layout
  "flex items-center justify-between",
  // Spacing
  "gap-4 p-4",
  // Background
  "bg-white"
)}>

Real-World Impact

Since implementing this in my own projects, I've noticed several improvements:

  1. Faster code reviews: Reviewers can quickly scan grouped classes to understand what's changing in each category.

  2. Easier debugging: When a spacing issue occurs, I go straight to the // Spacing group instead of scanning the entire class string.

  3. Better onboarding: New team members understand component styling faster when classes are semantically organized.

  4. Reduced merge conflicts: With classes on separate lines by category, Git diffs are cleaner and conflicts are easier to resolve.

Try It Out

Before installing anything, you can experiment with the grouping logic in the live playground. Paste your messy class strings and see them transformed instantly.

Contributing

This is an open-source project, and contributions are welcome! Whether it's:

  • Improving the classification patterns

  • Adding support for custom categories

  • Fixing edge cases

  • Improving documentation

Feel free to open issues or submit pull requests on GitHub.

Acknowledgments

Special thanks to Yangshun Tay for the LinkedIn post that sparked this project. Yangshun is an Ex-Meta Staff Engineer, the creator of Docusaurus 2, author of the widely-used "Blind 75" coding interview questions, and founder of GreatFrontEnd. His observation about Tailwind's readability challenges resonated with thousands of developers and gave me the motivation to build a solution. Sometimes the best tools come from simply acknowledging a shared frustration and deciding to do something about it.


About the Author

Ayowole Adenuga is a Senior Frontend Engineer with 6+ years of experience building scalable frontend architectures across multiple continents. He specializes in React, TypeScript, and modern frontend tooling. When he's not writing code, he's mentoring developers through non-profit organizations accross Africa or speaking at conferences about frontend engineering.

Connect with me:


If you found this helpful, consider giving the project a ⭐ on GitHub and sharing it with your team!