Tailwind CSS 73 🎨 Custom ESLint and Stylelint Rules for Enforcing Tailwind Utility Ordering
A Tailwind class list is a small program. The order of the classes does not affect the CSS cascade in most cases, because the utilities are generated in a fixed order and the specificity is the same. The order does affect the readability, the diff, the review, and the merge conflict. Two developers who add a class in different positions produce a conflict that has nothing to do with the logic. The prettier-plugin-tailwindcss is the tool that sorts the classes, and the ESLint and the Stylelint rules are the enforcement that keeps the order consistent across the team and the CI.
Key point: The prettier-plugin-tailwindcss sorts the classes in the markup. The ESLint plugin catches the deprecated and the invalid utilities, the conflicting classes, and the discouraged patterns. The Stylelint plugin catches the issues in the @apply and the custom CSS. The three tools are the different jobs, and they compose into the one pipeline.
Why the ordering rules exist
The consistency problem. The team has the different conventions: the “flex items-center” one, the “items-center flex” one, and the alphabetical one. The diff shows the reorder, and the review is the noise. The sorted order is the one convention.
The conflict problem. The two branches add the classes to the same element, and the merge conflict is the whole line. The sorted order does not remove the conflict, but it makes the resolution mechanical: the union of the two sets, sorted.
The review problem. The reviewer reads the class list to understand the element. The unsorted list hides the important class in the middle of the noise. The sorted list groups the related utilities, and the scan is the fast.
The deprecated problem. The v3 to the v4 renamed the utilities: the shadow-sm to the shadow-xs, the rounded to the rounded-sm, the outline-none to the outline-hidden. The ESLint plugin catches the deprecated names and suggests the replacement.
The conflict-class problem. The bg-red-500 bg-blue-500 is the conflict: the two classes set the same property, and the second wins. The ESLint plugin detects the conflict and warns.
The dynamic-class problem. The template literal produces the class the engine cannot see. The ESLint plugin has the rule that flags the dynamic construction, and the fix is the static form.
The plugin order problem. The prettier-plugin-tailwindcss must be the last plugin in the Prettier config. The plugin uses the Tailwind config to determine the sort order, and the wrong position produces the wrong order.
a. The Prettier plugin
The prettier-plugin-tailwindcss is the official plugin. It sorts the classes in the class and the className attributes, and it does not reorder the CSS or the @apply.
The installation
npm install -D prettier prettier-plugin-tailwindcss
The plugin is the dev dependency, and the prettier is the peer.
The configuration
// .prettierrc
{
"plugins": ["prettier-plugin-tailwindcss"]
}
The plugin is the last in the list. The order matters because the plugin needs the final AST.
The v4 configuration
// .prettierrc
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/app.css"
}
The tailwindStylesheet points to the CSS file with the @import "tailwindcss". The plugin reads the @theme and the @utility to determine the sort order.
The sort order
The plugin sorts by the utility category, then by the value.
// Before the sort
<div className="p-4 bg-blue-600 flex items-center text-white rounded-md">
// After the sort
<div className="flex items-center rounded-md bg-blue-600 p-4 text-white">
The order is the layout first, the box second, the background, the text, and the rest.
The sorted categories
| The order | The category |
|---|---|
| 1 | The layout (the display, the position) |
| 2 | The flexbox and the grid |
| 3 | The spacing (the margin, the padding) |
| 4 | The sizing (the width, the height) |
| 5 | The typography (the font, the text) |
| 6 | The background |
| 7 | The border |
| 8 | The effect (the shadow, the opacity) |
| 9 | The state (the hover, the focus) |
The exact order is defined by the plugin and the Tailwind’s internal order.
The function and the attributes
The plugin sorts the class and the className, the tw, the clsx, the cn, the cva, and the classnames.
// The cn
<div className={cn('p-4 flex', isActive && 'bg-blue-600')}>
// The cva
const button = cva('rounded px-4 py-2', { variants: { ... } });
The plugin recognizes the patterns and sorts the strings.
The ignore
The prettier-ignore comment skips the node.
{/* prettier-ignore */}
<div className="p-4 bg-blue-600 flex">
The plugin leaves the node unchanged.
The plugin table
| Element | Purpose |
|---|---|
prettier-plugin-tailwindcss | The sort |
tailwindStylesheet | The v4 config path |
tailwindConfig | The v3 config path |
The prettier-ignore | The skip |
The cn, the clsx, the cva | The recognized |
b. The ESLint plugin
The eslint-plugin-tailwindcss is the community plugin. It catches the issues the Prettier does not: the deprecated, the invalid, the conflicting, and the dynamic.
The installation
npm install -D eslint eslint-plugin-tailwindcss
The flat config
// eslint.config.js
import tailwind from 'eslint-plugin-tailwindcss';
export default [
...tailwind.configs['flat/recommended'],
];
The flat config spreads the recommended set.
The legacy config
// .eslintrc
{
"extends": ["plugin:tailwindcss/recommended"]
}
The legacy config extends the recommended.
The rules
| The rule | The purpose |
|---|---|
no-custom-classname | The warn on the unknown class |
no-contradicting-classname | The warn on the conflict |
enforces-shorthand | The suggest the shorthand |
no-unnecessary-arbitrary-value | The warn on the redundant |
classnames-order | The warn on the order |
no-arbitrary-value | The warn on the arbitrary |
no-dynamic-class-names | The warn on the template |
The no-contradicting-classname
// ❌ The conflict
<div className="bg-red-500 bg-blue-500">
// ✅ The conditional
<div className={isActive ? 'bg-blue-500' : 'bg-red-500'}>
The rule detects the two classes that set the same property.
The no-custom-classname
// ❌ The unknown class
<div className="bg-blue-600 my-custom-class">
The rule checks the class against the Tailwind’s known set.
The no-dynamic-class-names
// ❌ The dynamic
<div className={`text-${color}-600`}>
// ✅ The static
<div className={color === 'red' ? 'text-red-600' : 'text-blue-600'}>
The rule flags the template literal, and the fix is the static or the CSS variable.
The settings
export default [
...tailwind.configs['flat/recommended'],
{
settings: {
tailwindcss: {
config: 'tailwind.config.js',
cssConfigPath: 'src/app.css',
whitelist: ['my-custom-class'],
},
},
},
];
The config points to the v3 config, the cssConfigPath to the v4 CSS, and the whitelist lists the custom classes.
The ESLint table
| The setting | The purpose |
|---|---|
config | The v3 config |
cssConfigPath | The v4 CSS |
whitelist | The custom classes |
callees | The function names |
tags | The attribute names |
c. The Stylelint plugin
The stylelint-config-tailwindcss and the stylelint-plugin-tailwindcss are the CSS-side. They check the @apply and the custom CSS.
The installation
npm install -D stylelint stylelint-config-tailwindcss
The configuration
// .stylelintrc
{
"extends": ["stylelint-config-standard", "stylelint-config-tailwindcss"]
}
The config extends the standard and the Tailwind.
The at-rule checks
The plugin checks the @apply, the @theme, the @utility, and the @custom-variant.
/* ❌ The unknown utility in the @apply */
.button {
@apply bg-blue-600 my-unknown-utility;
}
/* ✅ The known */
.button {
@apply bg-blue-600 px-4 py-2;
}
The custom utility checks
@utility tab-4 {
tab-size: 4;
}
The plugin validates the custom utility syntax.
The rule list
| The rule | The purpose |
|---|---|
tailwindcss/no-unknown-apply | The unknown utility in the @apply |
tailwindcss/no-deprecated-apply | The deprecated utility |
tailwindcss/no-unknown-theme | The unknown @theme |
tailwindcss/no-unknown-utility | The unknown @utility |
The CSS order
The plugin also checks the order in the custom CSS, and it can enforce the @layer order.
The Stylelint table
| The config | The purpose |
|---|---|
stylelint-config-standard | The standard rules |
stylelint-config-tailwindcss | The Tailwind rules |
The @apply check | The unknown utility |
The @theme check | The unknown token |
Complete Example Session
// ============================================
// PART 1: THE PRETTIER CONFIG
// ============================================
// .prettierrc
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/app.css"
}
// ============================================
// PART 2: THE PRETTIER SORT
// ============================================
// Before
<div className="p-4 bg-blue-600 flex items-center text-white rounded-md">
// After
<div className="flex items-center rounded-md bg-blue-600 p-4 text-white">
// ============================================
// PART 3: THE ESLINT FLAT CONFIG
// ============================================
// eslint.config.js
import tailwind from 'eslint-plugin-tailwindcss';
export default [
...tailwind.configs['flat/recommended'],
{
settings: {
tailwindcss: {
cssConfigPath: 'src/app.css',
},
},
},
];
// ============================================
// PART 4: THE CONTRASTING CLASS
// ============================================
// ❌ The conflict
<div className="bg-red-500 bg-blue-500">
// ✅ The conditional
<div className={isActive ? 'bg-blue-500' : 'bg-red-500'}>
// ============================================
// PART 5: THE UNKNOWN CLASS
// ============================================
// ❌ The unknown
<div className="bg-blue-600 my-custom-class">
// ✅ The whitelist
{
settings: {
tailwindcss: {
whitelist: ['my-custom-class'],
},
},
}
// ============================================
// PART 6: THE DYNAMIC CLASS
// ============================================
// ❌ The dynamic
<div className={`text-${color}-600`}>
// ✅ The static
<div className={color === 'red' ? 'text-red-600' : 'text-blue-600'}>
// ============================================
// PART 7: THE STYLELINT CONFIG
// ============================================
// .stylelintrc
{
"extends": ["stylelint-config-standard", "stylelint-config-tailwindcss"]
}
// ============================================
// PART 8: THE APPLY CHECK
// ============================================
// ❌ The unknown utility in the @apply
.button {
@apply bg-blue-600 my-unknown-utility;
}
// ✅ The known
.button {
@apply bg-blue-600 px-4 py-2;
}
// ============================================
// PART 9: THE CVA AND THE CN
// ============================================
// The cn
<div className={cn('p-4 flex', isActive && 'bg-blue-600')}>
// The cva
const button = cva('rounded px-4 py-2', {
variants: {
intent: {
primary: 'bg-blue-600 text-white',
danger: 'bg-red-600 text-white',
},
},
});
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
// 1. Install the prettier-plugin-tailwindcss.
// 2. Set the tailwindStylesheet for the v4.
// 3. Install the eslint-plugin-tailwindcss.
// 4. Use the flat/recommended config.
// 5. Install the stylelint-config-tailwindcss.
// 6. Extend the standard and the Tailwind.
// 7. Use the whitelist for the custom classes.
// 8. Use the prettier-ignore for the exceptions.
// 9. Run the three in the CI.
// 10. The consistent order across the team.
The ten parts showed the Prettier config, the Prettier sort, the ESLint flat config, the contrasting class, the unknown class, the dynamic class, the Stylelint config, the @apply check, the cva and the cn, and the complete pattern.
Quick Reference
The Prettier Plugin
| Element | Purpose |
|---|---|
prettier-plugin-tailwindcss | The sort |
tailwindStylesheet | The v4 CSS |
tailwindConfig | The v3 config |
The prettier-ignore | The skip |
The cn, the clsx, the cva | The recognized |
The ESLint Rules
| The rule | The purpose |
|---|---|
no-custom-classname | The unknown class |
no-contradicting-classname | The conflict |
enforces-shorthand | The shorthand |
classnames-order | The order |
no-dynamic-class-names | The dynamic |
no-arbitrary-value | The arbitrary |
The ESLint Settings
| The setting | The purpose |
|---|---|
config | The v3 config |
cssConfigPath | The v4 CSS |
whitelist | The custom classes |
callees | The function names |
tags | The attribute names |
The Stylelint Rules
| The rule | The purpose |
|---|---|
no-unknown-apply | The unknown @apply |
no-deprecated-apply | The deprecated |
no-unknown-theme | The unknown @theme |
no-unknown-utility | The unknown @utility |
The Sort Categories
| The order | The category |
|---|---|
| 1 | The layout |
| 2 | The flexbox and the grid |
| 3 | The spacing |
| 4 | The sizing |
| 5 | The typography |
| 6 | The background |
| 7 | The border |
| 8 | The effect |
| 9 | The state |
Best Practices
✅ Do This:
// Use the prettier-plugin-tailwindcss.
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/app.css"
} // ✅
// Use the eslint flat/recommended.
import tailwind from 'eslint-plugin-tailwindcss';
export default [...tailwind.configs['flat/recommended']]; // ✅
// Use the whitelist for the custom classes.
settings: { tailwindcss: { whitelist: ['my-custom-class'] } } // ✅
// Use the static class.
className={color === 'red' ? 'text-red-600' : 'text-blue-600'} // ✅
// Use the cn and the cva.
className={cn('p-4 flex', isActive && 'bg-blue-600')} // ✅
// Use the stylelint config.
{
"extends": ["stylelint-config-standard", "stylelint-config-tailwindcss"]
} // ✅
❌ Don’t Do This:
// Don't put the plugin first.
{
"plugins": ["prettier-plugin-tailwindcss", "other-plugin"]
} // ❌ The plugin must be last
// Don't use the dynamic class.
className={`text-${color}-600`} // ❌ The engine cannot see
// Don't use the conflicting classes.
className="bg-red-500 bg-blue-500" // ❌ The conflict
/* Don't use the unknown utility in the @apply. */
.button { @apply my-unknown-utility; } // ❌
// Don't skip the prettier-ignore without the reason.
{/* prettier-ignore */}
<div className="..."> // ⚠️ Document the reason
// Don't forget the CSS config path.
settings: { tailwindcss: { cssConfigPath: 'src/app.css' } } // ✅
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The sort not working | The plugin not last | Move it last |
| The v4 config not read | No tailwindStylesheet | Add the path |
| The custom class flagged | Not in the whitelist | Add it |
| The dynamic class flagged | The template literal | Use the static |
| The conflict not caught | No ESLint | Add the plugin |
The @apply not checked | No Stylelint | Add the config |
| The conflicting merge | The unsorted classes | Use the sort |
Real-World Examples
1. The Prettier Config
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/app.css"
}
2. The Sorted Classes
<div className="flex items-center rounded-md bg-blue-600 p-4 text-white">
3. The ESLint Flat Config
import tailwind from 'eslint-plugin-tailwindcss';
export default [...tailwind.configs['flat/recommended']];
4. The Whitelist
settings: { tailwindcss: { whitelist: ['my-custom-class'] } }
5. The Static Class
className={color === 'red' ? 'text-red-600' : 'text-blue-600'}
6. The cn
className={cn('p-4 flex', isActive && 'bg-blue-600')}
7. The cva
const button = cva('rounded px-4 py-2', { variants: { ... } });
8. The Stylelint Config
{
"extends": ["stylelint-config-standard", "stylelint-config-tailwindcss"]
}
9. The @apply
.button { @apply bg-blue-600 px-4 py-2; }
10. The Complete Pattern
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/app.css"
}
Visual
The Tool Chain
┌────────────────────────────────────────────────────────────┐
│ THE TOOL CHAIN │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE PRETTIER │ │
│ │ The prettier-plugin-tailwindcss. │ │
│ │ The sort the classes. │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE ESLINT │ │
│ │ The eslint-plugin-tailwindcss. │ │
│ │ The deprecated, the conflict, the dynamic. │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE STYLELINT │ │
│ │ The stylelint-config-tailwindcss. │ │
│ │ The @apply, the @theme, the @utility. │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE CI │ │
│ │ The three run, the build fails on the error. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Prettier Sort
┌────────────────────────────────────────────────────────────┐
│ THE PRETTIER SORT │
│ │
│ THE BEFORE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ className="p-4 bg-blue-600 flex items-center │ │
│ │ text-white rounded-md" │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ THE AFTER │
│ ┌──────────────────────────────────────────────────┐ │
│ │ className="flex items-center rounded-md │ │
│ │ bg-blue-600 p-4 text-white" │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The order: the layout, the box, the background, the text. │
└────────────────────────────────────────────────────────────┘
The ESLint Rules
┌────────────────────────────────────────────────────────────┐
│ THE ESLINT RULES │
│ │
│ THE CONFLICT │
│ ┌──────────────────────────────────────────────────┐ │
│ │ className="bg-red-500 bg-blue-500" │ │
│ │ → the warn: the contradicting classname │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ THE DYNAMIC │
│ ┌──────────────────────────────────────────────────┐ │
│ │ className={`text-${color}-600`} │ │
│ │ → the warn: the dynamic class name │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ THE UNKNOWN │
│ ┌──────────────────────────────────────────────────┐ │
│ │ className="bg-blue-600 my-custom" │ │
│ │ → the warn: the custom classname │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The CI Pipeline
┌────────────────────────────────────────────────────────────┐
│ THE CI PIPELINE │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE PRETTIER CHECK │ │
│ │ npx prettier --check . │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE ESLINT CHECK │ │
│ │ npx eslint . │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE STYLELINT CHECK │ │
│ │ npx stylelint "**/*.css" │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE BUILD │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
prettier-plugin-tailwindcss | The sort the classes |
tailwindStylesheet | The v4 CSS path |
eslint-plugin-tailwindcss | The ESLint rules |
stylelint-config-tailwindcss | The Stylelint rules |
The no-contradicting-classname | The conflict detection |
The no-dynamic-class-names | The dynamic detection |
| The whitelist | The custom classes |
The prettier-ignore | The skip |
| The sort categories | The utility order |
| The CI check | The enforcement |
Key takeaways:
- The
prettier-plugin-tailwindcsssorts the classes. The plugin sorts by the utility category, and the order is the Tailwind’s internal. The plugin must be the last in the Prettier config. - The
tailwindStylesheetpoints to the v4 CSS. The plugin reads the@themeand the@utilityto determine the sort order. The v3 uses thetailwindConfig. - The
eslint-plugin-tailwindcsscatches the issues the Prettier does not. Theno-contradicting-classnamedetects the conflict, theno-dynamic-class-namesdetects the template literal, and theno-custom-classnamedetects the unknown. - The
stylelint-config-tailwindcsschecks the CSS. The@apply, the@theme, and the@utilityare the checked. The unknown utility in the@applyis the error. - The whitelist lists the custom classes. The plugin would flag the custom class as the unknown, and the whitelist is the exception. The list is the configuration.
- The
prettier-ignoreskips the node. The comment is the exception, and the plugin leaves the node unchanged. The reason should be the documented. - The three tools compose into the CI pipeline. The Prettier check, the ESLint check, and the Stylelint check run in the CI, and the build fails on the error.
- The consistent order reduces the merge conflict. The sorted order does not remove the conflict, but the resolution is the mechanical: the union of the two sets, sorted.
Remember: The prettier-plugin-tailwindcss, the eslint-plugin-tailwindcss, and the stylelint-config-tailwindcss are the enforcement. The Prettier sorts, the ESLint catches the issues, and the Stylelint checks the CSS. The result is the consistent order across the team, the clean diff, and the CI that catches the regressions.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!