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

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-0Layout:
flex,items-center,justify-betweenSpacing:
px-3,py-2,mt-4Borders:
border,rounded-md,outline-noneColors:
bg-white,text-gray-900,placeholder:text-mutedStates:
hover:shadow-md,focus:ring-2,disabled:opacity-50Responsive:
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
Homepage & Playground: https://ayowoleadenuga.github.io/tailwind-class-grouper
GitHub Repository: https://github.com/ayowoleadenuga/tailwind-class-grouper
ESLint Plugin: https://www.npmjs.com/package/eslint-plugin-tailwind-group
Prettier Plugin: https://www.npmjs.com/package/prettier-plugin-tailwind-group
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:
- 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...
- 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
- 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 likeclassnames) is available in your project, as the grouped output is wrapped inclsx(...).
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:
It's already ubiquitous: Most React projects using Tailwind already have
clsxorclassnamesinstalled for conditional class handling.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.
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:
Faster code reviews: Reviewers can quickly scan grouped classes to understand what's changing in each category.
Easier debugging: When a spacing issue occurs, I go straight to the
// Spacinggroup instead of scanning the entire class string.Better onboarding: New team members understand component styling faster when classes are semantically organized.
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:
Github: @ayowoleadenuga
LinkedIn: Ayowole Adenuga
If you found this helpful, consider giving the project a ⭐ on GitHub and sharing it with your team!