ESLint plugin that enforces best practices and consistency when using Tailwind CSS utility classes
npx @tessl/cli install tessl/npm-eslint-plugin-tailwindcss@2.0.0ESLint Plugin Tailwind CSS is a comprehensive ESLint plugin that enforces best practices and consistency while using Tailwind CSS utility classes. It provides three powerful rules that automatically parse your tailwind.config.js file to validate classes against your project configuration, supports JIT mode features including arbitrary values and color opacity shorthand, and prevents common issues like contradicting classnames and invalid custom classes.
npm install eslint-plugin-tailwindcss --save-devESLint plugins are not directly imported in code. Instead, they are configured in your ESLint configuration file:
{
"plugins": ["tailwindcss"]
}Use the recommended preset for sensible defaults:
{
"extends": ["plugin:tailwindcss/recommended"]
}Or configure individual rules:
{
"plugins": ["tailwindcss"],
"rules": {
"tailwindcss/classnames-order": "warn",
"tailwindcss/no-custom-classname": "warn",
"tailwindcss/no-contradicting-classname": "error"
}
}The plugin is structured around three core components:
tailwind.config.js files to understand project-specific configurationsMain plugin export containing all rules and preset configurations.
module.exports = {
rules: {
'classnames-order': Rule,
'no-contradicting-classname': Rule,
'no-custom-classname': Rule
},
configs: {
recommended: ESLintConfig
}
};
interface ESLintConfig {
plugins: string[];
parserOptions: {
ecmaFeatures: {
jsx: boolean;
};
};
rules: {
[key: string]: string | [string, object];
};
}Enforces consistent and logical ordering of Tailwind CSS classnames based on CSS properties and variants.
/**
* ESLint rule that enforces consistent ordering of Tailwind CSS classnames
* Supports custom grouping, responsive variants, and automatic fixing
*/
const classnamesOrderRule = {
meta: {
docs: {
description: string;
category: 'Stylistic Issues';
recommended: boolean;
url: string;
};
messages: {
invalidOrder: string;
};
fixable: 'code';
schema: RuleSchema[];
};
create: (context: ESLintContext) => ESLintVisitors;
};
interface ClassnamesOrderOptions {
/** Array of function names to inspect for classnames (default: ["classnames", "clsx", "ctl"]) */
callees?: string[];
/** Path to tailwind config file or config object (default: "tailwind.config.js") */
config?: string | object;
/** Array defining classname grouping and ordering rules */
groups?: GroupDefinition[];
/** Place custom classes before Tailwind classes (default: false) */
prependCustom?: boolean;
/** Remove duplicate classnames (default: true) */
removeDuplicates?: boolean;
/** Array of JSX tag names to inspect */
tags?: string[];
/** Array of whitelisted custom classnames */
whitelist?: string[];
}
interface GroupDefinition {
type: string;
members: string | string[] | GroupDefinition[];
}Prevents contradicting Tailwind CSS classnames that target the same CSS property with different values.
/**
* ESLint rule that detects contradicting Tailwind CSS classnames
* Examples: "w-3 w-5", "text-red-500 text-blue-500", "p-2 p-4"
*/
const noContradictingClassnameRule = {
meta: {
docs: {
description: string;
category: 'Possible Errors';
recommended: boolean;
url: string;
};
messages: {
conflictingClassnames: string;
};
fixable: null;
schema: RuleSchema[];
};
create: (context: ESLintContext) => ESLintVisitors;
};
interface NoContradictingClassnameOptions {
/** Array of function names to inspect for classnames */
callees?: string[];
/** Path to tailwind config file or config object */
config?: string | object;
/** Array of JSX tag names to inspect */
tags?: string[];
/** Array of whitelisted classnames to ignore conflicts for */
whitelist?: string[];
}Detects classnames that don't belong to Tailwind CSS, helping maintain consistency and catch typos.
/**
* ESLint rule that detects non-Tailwind CSS classnames
* Can scan CSS files to allow project-specific classes
*/
const noCustomClassnameRule = {
meta: {
docs: {
description: string;
category: 'Best Practices';
recommended: boolean;
url: string;
};
messages: {
customClassnameDetected: string;
};
fixable: null;
schema: RuleSchema[];
};
create: (context: ESLintContext) => ESLintVisitors;
};
interface NoCustomClassnameOptions {
/** Array of function names to inspect for classnames */
callees?: string[];
/** Path to tailwind config file or config object */
config?: string | object;
/** Array of glob patterns for CSS files to scan for valid classnames (default: ["**/*.css", "!**/node_modules", "!**/.*", "!**/dist", "!**/build"]) */
cssFiles?: string[];
/** Milliseconds between CSS file rescans (default: 5000) */
cssFilesRefreshRate?: number;
/** Array of JSX tag names to inspect */
tags?: string[];
/** Array of whitelisted custom classnames to allow */
whitelist?: string[];
}Global settings that apply to all rules in the plugin, reducing configuration duplication.
/**
* Shared settings for all eslint-plugin-tailwindcss rules
* Configure once in eslint config settings.tailwindcss
*/
interface SharedSettings {
/** Array of function names to inspect for classnames across all rules (default: ["classnames", "clsx", "ctl"]) */
callees?: string[];
/** Path to tailwind config file or config object (default: "tailwind.config.js") */
config?: string | object;
/** Array of glob patterns for CSS files (default: ["**/*.css", "!**/node_modules", "!**/.*", "!**/dist", "!**/build"]) */
cssFiles?: string[];
/** Milliseconds between CSS file rescans for no-custom-classname (default: 5000) */
cssFilesRefreshRate?: number;
/** Group responsive variants together in classnames-order (default: false) */
groupByResponsive?: boolean;
/** Custom grouping configuration for classnames-order */
groups?: GroupDefinition[];
/** Place custom classes before Tailwind classes (default: false) */
prependCustom?: boolean;
/** Remove duplicate classnames (default: true) */
removeDuplicates?: boolean;
/** Array of JSX tag names to inspect (default: []) */
tags?: string[];
/** Array of whitelisted custom classnames (default: []) */
whitelist?: string[];
}interface ESLintContext {
/** Report a linting issue */
report: (descriptor: ReportDescriptor) => void;
/** Get rule options */
options: any[];
/** Get settings from eslint config */
settings: {
tailwindcss?: SharedSettings;
};
/** Get source code methods */
getSourceCode: () => SourceCode;
}
interface ESLintVisitors {
/** Visit JSX attributes */
JSXAttribute?: (node: JSXAttributeNode) => void;
/** Visit call expressions */
CallExpression?: (node: CallExpressionNode) => void;
/** Visit template literals */
TemplateLiteral?: (node: TemplateLiteralNode) => void;
/** Visit property assignments */
Property?: (node: PropertyNode) => void;
}
interface ReportDescriptor {
/** AST node to report on */
node: ASTNode;
/** Message identifier from rule meta.messages */
messageId: string;
/** Data to interpolate into message */
data?: object;
/** Optional fix function for auto-fixable rules */
fix?: (fixer: RuleFixer) => Fix;
}
interface RuleSchema {
type: 'object' | 'array' | 'string' | 'boolean' | 'number';
properties?: {
[key: string]: RuleSchema;
};
items?: RuleSchema;
default?: any;
}The plugin handles various error conditions gracefully:
Common error messages:
"Invalid Tailwind CSS classnames order" - When classnames are not in the expected order"Classnames {{classnames}} are conflicting!" - When contradicting classes are detected"Classname '{{classname}}' is not a Tailwind CSS class!" - When non-Tailwind classes are found{
"plugins": ["tailwindcss"],
"extends": ["plugin:tailwindcss/recommended"]
}{
"plugins": ["tailwindcss"],
"settings": {
"tailwindcss": {
"config": "./custom-tailwind.config.js",
"callees": ["classnames", "clsx", "cn"],
"whitelist": ["my-custom-class"],
"cssFiles": ["src/**/*.css"]
}
},
"rules": {
"tailwindcss/classnames-order": ["warn", {
"groupByResponsive": true,
"removeDuplicates": true
}],
"tailwindcss/no-custom-classname": ["error", {
"cssFilesRefreshRate": 10000
}],
"tailwindcss/no-contradicting-classname": "error"
}
}{
"plugins": ["tailwindcss"],
"extends": ["plugin:tailwindcss/recommended"],
"settings": {
"tailwindcss": {
"callees": ["classnames", "clsx", "cn", "tw"],
"tags": ["div", "span", "section", "Button", "Card"]
}
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
}
}