@shirakaba/eslint-plugin-tailwind-canonical-classes
v2.0.0
Published
Fast ESLint enforcement for canonical Tailwind CSS v4 class names
Maintainers
Readme
@shirakaba/eslint-plugin-tailwind-canonical-classes
A speedy fork of eslint-plugin-tailwind-canonical-classes that works by running tailwind-canonicalize in-process rather than spinning up a @tailwindcss/node worker.
| Measurement | Original (v1.4.1) | This fork (v2.0.0) | | -------------------- | ----------------: | -----------------: | | Plugin rule time | 1,684.8 ms | 4.1 ms | | Next app lint median | 2.86 s | 1.06 s |
Fast, auto-fixable ESLint enforcement for canonical Tailwind CSS v4 class names.
📋 Table of Contents
- Features
- 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 the same safe canonicalization engine as
tailwind-canonicalize - 📝 Multiple Format Support: Works with string literals, template literals, and JSX expressions
- 🛠️ Utility Function Support: Detects and fixes classes in utility functions like
cn(),clsx(),classNames(),twMerge(), andcva() - ⚡ Low Overhead: Runs in-process with shared theme and candidate caches—no worker or Tailwind compiler startup
📦 Installation
Install the plugin, aliasing it to eslint-plugin-tailwind-canonical-classes:
npm install --save-dev eslint-plugin-tailwind-canonical-classes@npm:@shirakaba/eslint-plugin-tailwind-canonical-classesOr with yarn:
yarn add -D eslint-plugin-tailwind-canonical-classes@npm:@shirakaba/eslint-plugin-tailwind-canonical-classesOr with pnpm:
pnpm add -D eslint-plugin-tailwind-canonical-classes@npm:@shirakaba/eslint-plugin-tailwind-canonical-classesRequirements
- Node.js >= 20.19.0
- ESLint 9 or 10 using flat config
- Tailwind CSS v4
- 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.*) |
| :------------: | :-----------------------------: |
| 9.x | Supported |
| 10.x | Supported |
Legacy .eslintrc configuration is not supported.
🚀 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
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']
},
],
},
},
];Svelte (Flat Config)
Install the Svelte parser alongside the plugin:
npm install --save-dev svelte-eslint-parserThen 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-parserThen 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",
},
],
},
},
];📖 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 pathrootFontSize (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 14pxcalleeFunctions (optional)
- Type:
string[] - Default:
['cn', 'clsx', 'classNames', 'twMerge', 'cva'] - Description: Array of utility function names to check for Tailwind classes. The plugin will detect and canonicalize classes passed as string arguments to these functions.
Example:
calleeFunctions: ["cn", "clsx"]; // Only check cn() and clsx()
calleeFunctions: ["customFn"]; // Check a custom utility function💡 Usage Examples
Basic Example
Before:
<div className="p-[16px] 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 (static parts only):
<div className={`p-4 ${someVar}`}>Content</div> // Only "p-4" will be checked, dynamic parts are skippedJSX expression containers:
<div className={"p-[16px]"}>Content</div>Utility functions (e.g.,
cn(),clsx(),classNames(),twMerge(),cva()):<div className={cn("p-[16px]", "m-[2rem]")}>Content</div> <div className={clsx("w-[16px]", condition && "hidden")}>Content</div> // Only string literal arguments are checked; dynamic expressions 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 Theme: The plugin reads the CSS file once and extracts Tailwind v4
@themevalues. Parsed themes and canonicalization results are shared across linted files. - Extract Classes: It extracts class names from:
- JSX
classNameattributes (string literals, template literals, JSX expressions) - Standalone script-level utility function calls (e.g.
cn(),clsx()) outside class attributes - Svelte
classattributes (static literals and mustachecn()/clsx()calls) - Vue
class,:class, andv-bind:classattributes (static literals andcn()/clsx()calls) - Utility function calls inside class attributes — only string literal arguments are checked
- JSX
- Canonicalize: It applies the safe, synchronous
tailwind-canonicalizeresolver in-process. There is no worker startup or Tailwind compiler initialization. - Report & Fix: If a non-canonical class is found, it reports an error/warning and can auto-fix it
⚠️ 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: Static template literals are supported; template literals containing expressions are skipped
- Utility functions: Only string literal arguments are checked; dynamic expressions, variables, and conditional logic within utility functions are skipped
- Framework parsers:
flat/svelteandflat/vuerequiresvelte-eslint-parserandvue-eslint-parserrespectively - Vue
:classobject/array forms: Object and array:classbindings (for example{ 'w-4': true }or['w-4', dynamic]) are skipped
🐛 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
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/shirakaba/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 testRelease 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
