eslint-plugin-tailwind-canonical-classes
| Install | |
|---|---|
npm install eslint-plugin-tailwind-canonical-classes |
|
| Latest Version: | 1.6.0 |
| License: | MIT |
| Last Updated: | Oct 20, 2026 |
| Links: | GitHub · npm |
eslint-plugin-tailwind-canonical-classes
ESLint plugin to enforce canonical Tailwind CSS class names using Tailwind CSS v4's canonicalization API.
📋 Table of Contents
- Features
- Comparison
- Installation
- Quick Start
- Configuration
- Options
- Usage Examples
- How It Works
- Limitations
- Troubleshooting
- Contributing
- License
- Related Links
✨ Features
- 🔍 Automatic Detection: Automatically detects non-canonical Tailwind CSS class names in JSX/TSX, Svelte, and Vue files
- 🔧 Auto-fix Support: Automatically fixes non-canonical classes using ESLint's auto-fix feature
- 🎯 Tailwind CSS v4 Integration: Uses Tailwind CSS v4's official
canonicalizeCandidatesAPI - 📝 Multiple Format Support: Works with string literals, template literals, ternaries,
&&/||, arrays, and object keys - 🛠️ Utility Function Support: Detects and fixes classes in utility functions like
cn(),clsx(),classNames(),twMerge(),cva(), andtv(), and intw`…`tagged templates - ⚡ Zero Config: Minimal configuration required to get started
⚖️ Comparison
Two larger plugins also ship a canonical-classes rule built on the same Tailwind API: eslint-plugin-tailwindcss (enforces-canonical-classname) and eslint-plugin-better-tailwindcss (enforce-canonical-classes). Choose this plugin when you want only canonical class names, with no other Tailwind rules to configure.
| This plugin | eslint-plugin-tailwindcss |
eslint-plugin-better-tailwindcss |
|
|---|---|---|---|
| Scope | Canonical classes only | Full Tailwind rule set | Full Tailwind rule set |
px → spacing scale (p-[16px] → p-4) |
On by default | No | With rootFontSize |
Two classes with the same canonical form (p-[1rem] p-4) |
Fixes only p-[1rem] |
Rewrites the wrong classes | Not reported |
| Rule time, 400 JSX files, 12k class strings, first run | 1.2 s | 1.2 s | 9.8 s |
| Same, next runs (on-disk cache) | 0.1 s | 1.2 s | 9.8 s |
Ternaries, template literals with ${}, clsx object keys, Vue :class object/array bindings |
Checked | Checked | Checked |
cva variants, tv(), tw`…` tagged templates |
Checked | Checked | Checked |
Measured on 2026-10-20 with ESLint 9.39, Tailwind CSS 4.3.3, eslint-plugin-tailwindcss 4.4.0, and eslint-plugin-better-tailwindcss 4.10.0. Rule time is total lint time minus a run with the rule off, median of five runs.
📦 Installation
Install the plugin and its peer dependency:
npm install --save-dev eslint-plugin-tailwind-canonical-classes @tailwindcss/node
Or with yarn:
yarn add -D eslint-plugin-tailwind-canonical-classes @tailwindcss/node
Or with pnpm:
pnpm add -D eslint-plugin-tailwind-canonical-classes @tailwindcss/node
Requirements
- Node.js >= 18.0.0
- ESLint 8, 9, or 10
- Tailwind CSS v4
- @tailwindcss/node package
- svelte-eslint-parser (optional, required for Svelte support)
- vue-eslint-parser (optional, required for Vue support)
ESLint Version Compatibility
| ESLint Version | Flat Config (eslint.config.*) |
Legacy Config (.eslintrc.*) |
|---|---|---|
| 8.x | Supported | Supported |
| 9.x | Supported | Supported |
| 10.x | Supported | Not available (removed by ESLint) |
Note: ESLint 10 removes
.eslintrcsupport entirely. If you are upgrading to ESLint 10, you must migrate to flat config.
🚀 Quick Start
-
Install the plugin (see Installation)
-
Add to your ESLint config (
eslint.config.mjs):import tailwindCanonicalClasses from 'eslint-plugin-tailwind-canonical-classes'; export default [ ...tailwindCanonicalClasses.configs['flat/recommended'], { rules: { 'tailwind-canonical-classes/tailwind-canonical-classes': [ 'warn', { cssPath: './app/styles/globals.css', }, ], }, }, ]; -
Run ESLint:
npx eslint . --fix
⚙️ Configuration
Flat Config (ESLint 8+) — Recommended
Using the built-in flat/recommended config:
import tailwindCanonicalClasses from 'eslint-plugin-tailwind-canonical-classes';
export default [
...tailwindCanonicalClasses.configs['flat/recommended'],
{
rules: {
'tailwind-canonical-classes/tailwind-canonical-classes': [
'warn', // or 'error'
{
cssPath: './app/styles/globals.css', // Required
rootFontSize: 16, // Optional, default: 16
calleeFunctions: ['cn', 'clsx'], // Optional, default: ['cn', 'clsx', 'classNames', 'twMerge', 'cva', 'tv', 'tw']
},
],
},
},
];
Svelte (Flat Config)
Install the Svelte parser alongside the plugin:
npm install --save-dev svelte-eslint-parser
Then extend the built-in config:
import tailwindCanonicalClasses from 'eslint-plugin-tailwind-canonical-classes';
export default [
...tailwindCanonicalClasses.configs['flat/svelte'],
{
rules: {
'tailwind-canonical-classes/tailwind-canonical-classes': [
'warn',
{
cssPath: './src/app.css',
},
],
},
},
];
Vue (Flat Config)
Install the Vue parser alongside the plugin:
npm install --save-dev vue-eslint-parser
Then extend the built-in config:
import tailwindCanonicalClasses from 'eslint-plugin-tailwind-canonical-classes';
export default [
...tailwindCanonicalClasses.configs['flat/vue'],
{
rules: {
'tailwind-canonical-classes/tailwind-canonical-classes': [
'warn',
{
cssPath: './src/style.css',
},
],
},
},
];
Or with manual plugin registration:
import tailwindCanonicalClasses from 'eslint-plugin-tailwind-canonical-classes';
export default [
{
plugins: {
'tailwind-canonical-classes': tailwindCanonicalClasses,
},
rules: {
'tailwind-canonical-classes/tailwind-canonical-classes': [
'warn',
{
cssPath: './app/styles/globals.css',
},
],
},
},
];
Legacy Config (.eslintrc.js) — ESLint 8/9 only
Deprecated: Legacy config (
.eslintrc) is not supported by ESLint 10. Migrate to flat config above.
module.exports = {
plugins: ['tailwind-canonical-classes'],
rules: {
'tailwind-canonical-classes/tailwind-canonical-classes': [
'warn',
{
cssPath: './app/styles/globals.css',
rootFontSize: 16,
calleeFunctions: ['cn', 'clsx'],
},
],
},
};
📖 Options
cssPath (required)
- Type:
string - Description: Path to your Tailwind CSS file
- Supported formats:
- Relative path: Resolved relative to your project root (where ESLint config is located)
- Absolute path: Full filesystem path to your CSS file
Examples:
cssPath: './app/styles/globals.css' // Relative to project root
cssPath: './src/index.css' // Another relative example
cssPath: '/absolute/path/to/styles.css' // Absolute path
rootFontSize (optional)
- Type:
number - Default:
16 - Description: Root font size in pixels for rem calculations. This should match your CSS root font size setting.
Example:
rootFontSize: 16 // Default (16px = 1rem)
rootFontSize: 14 // If your root font size is 14px
calleeFunctions (optional)
- Type:
string[] - Default:
['cn', 'clsx', 'classNames', 'twMerge', 'cva', 'tv', 'tw'] - Description: Names of functions whose arguments hold Tailwind classes. A name also matches tagged templates, such as
tw`…`andtw.div`…`. Forcvaandtv, classes are read from the config values (base,slots, variant options, andclass/classNamein compound entries);defaultVariantsis skipped.
Example:
calleeFunctions: ['cn', 'clsx'] // Only check cn() and clsx()
calleeFunctions: ['customFn'] // Check a custom utility function
💡 Usage Examples
Basic Example
Before:
<div className="p-4px m-2rem">Content</div>
After auto-fix:
<div className="p-1 m-8">Content</div>
Supported Formats
The plugin supports various class name formats:
-
String literals:
<div className="p-4 m-2">Content</div> -
Template literals, including ones with
${}expressions:<div className={`p-4px ${size} m-2rem`}>Content</div> // A class touching ${} (such as bg-${color}) is only part of a class and is skipped -
Conditional expressions, arrays, and object keys:
<div className={active ? "p-4px" : "m-2rem"}>Content</div> <div className={clsx({ "w-[16px]": active }, ["h-[32px]"])}>Content</div> -
Utility functions (e.g.,
cn(),clsx(),classNames(),twMerge(),cva()):<div className={cn("p-4px", "m-2rem")}>Content</div> <div className={clsx("w-[16px]", condition && "hidden")}>Content</div> const button = cva("w-[16px]", { variants: { size: { sm: "h-[32px]" } } }); const card = tv({ base: "p-4px", slots: { title: "m-2rem" } }); const Title = tw.h1`text-[16px]`; // Variables and other dynamic values are skipped
Real-world Example
// Before
function Card({ children }) {
return (
<div className="p-16px m-2rem rounded-8px shadow-lg">
{children}
</div>
);
}
// After auto-fix
function Card({ children }) {
return (
<div className="p-4 m-8 rounded-2 shadow-lg">
{children}
</div>
);
}
Utility Function Example
// Before
import { cn } from '@/lib/utils';
function Button({ variant, className }) {
return (
<button className={cn("w-[16px]", "h-[32px]", className)}>
Click me
</button>
);
}
// After auto-fix
import { cn } from '@/lib/utils';
function Button({ variant, className }) {
return (
<button className={cn("w-4", "h-8", className)}>
Click me
</button>
);
}
🔧 How It Works
- Load Design System: The plugin loads your Tailwind CSS file using
@tailwindcss/node's worker API - Extract Classes: It extracts class names from:
- JSX
classNameattributes - Standalone script-level utility function calls (e.g.
cn(),clsx()) outside class attributes - Svelte
classattributes, static text and{...}expressions - Vue
class,:class, andv-bind:classattributes, including object and array bindings - Inside expressions it checks string literals, template literal text, both sides of
? :,&&, and||, array elements, object keys, and arguments of nested utility calls
- JSX
- Canonicalize: For each class, it uses Tailwind's
canonicalizeCandidatesto find the canonical form - Report & Fix: If a non-canonical class is found, it reports an error/warning and can auto-fix it
Cache
Tailwind needs about a second to build its canonicalization index the first time it sees a class. To skip that on later runs, the plugin stores every class it has canonicalized in node_modules/.cache/eslint-plugin-tailwind-canonical-classes/ under the ESLint working directory, or in the system temp directory when there is no node_modules. A run that finds all of its classes there never starts Tailwind.
The cache is tied to the plugin version, the Tailwind version the plugin runs, rootFontSize, and every file Tailwind read to load your CSS: the stylesheet, its local imports, and imported package files such as tailwindcss/index.css. Editing or upgrading any of them starts a fresh cache, including inside a running editor or eslint_d. Old cache files are never pruned; delete the directory to reclaim space.
Set TAILWIND_CANONICAL_CLASSES_CACHE_DIR to store the cache somewhere else, for example a directory your CI caches between runs.
⚠️ Limitations
- Static classes only: Only works with static class names (no dynamic expressions)
- Tailwind CSS v4 required: Requires Tailwind CSS v4 (not compatible with v3)
- CSS file accessibility: CSS file must be accessible from the ESLint process
- Template literals: A class touching a
${}expression (for examplebg-${color}) is skipped - Dynamic values: Variables, computed object keys, and
clsxobject values are skipped cva/tvclass objects: Aclsx-style object written directly in acvaortvconfig (for examplebase: { 'w-4': active }) is read as config, so its class keys are skipped; wrap it inclsx()to have it checked- Tagged templates: Only
tw`…`andtw.<tag>`…`match; deeper chains such astw.colors.div`…`are skipped - Framework parsers:
flat/svelteandflat/vuerequiresvelte-eslint-parserandvue-eslint-parserrespectively
🐛 Troubleshooting
Plugin not detecting classes
Problem: The plugin isn't reporting any issues with non-canonical classes.
Solutions:
- Verify your
cssPathis correct and points to a valid Tailwind CSS file - Ensure the CSS file is accessible from where ESLint runs
- Check that your Tailwind CSS file contains valid Tailwind directives (
@import "tailwindcss"or similar) - Verify ESLint is processing your JSX/TSX files (check your ESLint config includes these file types)
Path resolution issues
Problem: ESLint can't find your CSS file.
Solutions:
- Use an absolute path if relative paths aren't working
- Ensure the path is relative to your ESLint config file location
- Check file permissions
"Tailwind could not canonicalize classes"
Problem: ESLint reports Tailwind could not canonicalize classes with <path>: <error> once per file and checks no classes in it.
Solutions:
- Tailwind failed to load your CSS file. The message ends with Tailwind's own error.
- Fix the import, plugin, or syntax it names, for example a package that is not installed in this project
Auto-fix not working
Problem: ESLint reports issues but doesn't auto-fix them.
Solutions:
- Run ESLint with the
--fixflag:npx eslint . --fix - Ensure your editor's ESLint extension has auto-fix enabled
- Check that the rule severity is set to
'warn'or'error'(not'off')
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup
# Clone the repository
git clone https://github.com/MaisonnatM/eslint-plugin-tailwind-canonical-classes.git
cd eslint-plugin-tailwind-canonical-classes
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm test
Release Process
This project uses semantic-release for automated version management and npm publishing. Releases are automatically triggered when commits are pushed to the main branch.
Commit Message Format:
fix:- Patch release (1.0.8 → 1.0.9)feat:- Minor release (1.0.8 → 1.1.0)feat!:orBREAKING CHANGE:- Major release (1.0.8 → 2.0.0)
📄 License
MIT © Maisonnat Maxence
🔗 Related Links
Related Packages
Normalize Tailwinds: Tailwind shorthand audit, canonicalization checks, and safe...
An ESLint plugin that provides rules to verify CSS definition objects.
ESLint plugin for the complete Nagi CSS contract across templates and component-...
ESLint rules that keep utility classes on the element they style, for Tailwind C...
Version History
| Version | Released | License |
|---|---|---|
| 1.6.0 | MIT | |
| 1.5.0 | MIT | |
| 1.4.4 | MIT | |
| 1.4.3 | MIT | |
| 1.4.2 | MIT | |
| 1.4.1 | MIT | |
| 1.4.0 | MIT | |
| 1.3.3 | MIT | |
| 1.3.2 | MIT | |
| 1.3.1 | MIT | |
| 1.3.0 | MIT | |
| 1.2.0 | MIT | |
| 1.1.0 | MIT | |
| 1.0.10 | MIT | |
| 1.0.8 | MIT | |
| 1.0.7 | MIT | |
| 1.0.6 | MIT | |
| 1.0.3 | MIT | |
| 1.0.2 | MIT | |
| 1.0.1 | MIT | |
| 1.0.0 | MIT |