ESLint plugin for Tailwind CSS v4 with advanced linting rules.
- 🎯 Tailwind CSS v4 Support - Full support for the latest Tailwind syntax
- 🔍 Smart Validation - Validates directives, modifiers, and utility classes
- 🚀 Auto-fixable Rules - Many rules can automatically fix issues
- 📦 Preset Configurations - Ready-to-use configs for different strictness levels
- 🎨 Theme-aware - Encourages consistent use of design tokens
- ⚡ Performance - Optimized for large codebases
- ESLint 9.30.0 or higher
- Node.js 20.19.0 or higher
- Flat config format (eslint.config.js)
# pnpm
pnpm add -D @poupe/eslint-plugin-tailwindcss
# npm
npm install -D @poupe/eslint-plugin-tailwindcss
# yarn
yarn add -D @poupe/eslint-plugin-tailwindcssNote: The plugin registers its own tailwindcss/css language
(built on @eslint/css). If you also want css/* rules from
@eslint/css, import that plugin separately in your config
or use @poupe/eslint-config.
Create an eslint.config.mjs file:
// @ts-check
import tailwindcss from '@poupe/eslint-plugin-tailwindcss';
export default [
tailwindcss.configs.recommended,
];Each preset config includes file globs (**/*.?(post)css),
tailwindcss/css language with Tailwind v4 syntax, and the plugin
self-reference.
Override specific rules after a preset:
// @ts-check
import tailwindcss from '@poupe/eslint-plugin-tailwindcss';
export default [
tailwindcss.configs.recommended,
{
rules: {
'tailwindcss/no-arbitrary-value-overuse': 'error',
},
},
];Setup-only config with no rules. Provides file globs (**/*.?(post)css),
tailwindcss/css language, tolerant parsing, Tailwind v4 custom syntax,
and the plugin self-reference. All other presets extend base.
Basic syntax validation only. Good for existing projects.
- ✅
valid-theme-function - ✅
valid-modifier-syntax - ✅
valid-apply-directive - ✅
no-invalid-at-rules - ✅
no-invalid-named-grid-areas - ✅
no-invalid-properties - ✅
no-duplicate-imports - ✅
no-duplicate-reference - ✅
no-empty-blocks - ✅
no-important - ✅
require-reference-in-vue - ✅
use-baseline
Balanced set of rules for most projects.
- ✅ All rules from
minimal(no-importantrelaxed to warning) - ✅
no-conflicting-utilities ⚠️ no-arbitrary-value-overuse(warning)⚠️ prefer-theme-tokens(warning)
All rules enabled with strict settings. Best for new projects.
- ✅ All rules as errors
- ✅ Strictest configuration options
- ✅ Includes additional rules (consistent spacing, logical properties, relative units, layers)
Here's a comprehensive eslint.config.mjs with TypeScript support:
// @ts-check
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import tailwindcss from '@poupe/eslint-plugin-tailwindcss';
export default [
js.configs.recommended,
...tseslint.configs.recommended,
tailwindcss.configs.recommended,
];Rules that catch potential bugs or invalid syntax.
| Rule | Description | 🔧 |
|---|---|---|
| no-conflicting-utilities | Detects conflicting Tailwind utilities that affect the same CSS properties | |
| no-duplicate-imports | Disallow duplicate @import rules | |
| no-duplicate-reference | Disallow duplicate @reference directives | |
| no-empty-blocks | Disallow empty rule blocks and at-rule blocks | |
| no-invalid-at-rules | Disallow invalid at-rule names and syntax | |
| no-invalid-named-grid-areas | Disallow invalid named grid areas in CSS Grid templates | |
| no-invalid-properties | Disallow invalid CSS property names | |
| require-reference-in-vue | Require @reference directive in Vue SFC style blocks | 🔧 |
| use-baseline | Enforce use of widely-supported CSS features | |
| valid-apply-directive | Validates the @apply directive usage |
|
| valid-modifier-syntax | Ensures Tailwind modifiers follow correct syntax patterns | |
| valid-theme-function | Validates usage of the theme() function in CSS files |
🔧 |
Rules that guide towards better code patterns and maintainability.
| Rule | Description | 🔧 |
|---|---|---|
| no-arbitrary-value-overuse | Warns when too many arbitrary values are used instead of theme tokens | |
| no-important | Discourage use of !important | |
| prefer-logical-properties | Enforce the use of logical properties over physical properties | 🔧 |
| prefer-theme-tokens | Suggests using theme tokens instead of hard-coded values | |
| relative-font-units | Prefer relative units (rem/em) over absolute (px) for fonts | |
| use-layers | Encourage use of @layer for better CSS architecture |
Rules that enforce code style and formatting conventions.
| Rule | Description | 🔧 |
|---|---|---|
| consistent-spacing | Enforces consistent spacing around colons in CSS declarations | 🔧 |
🔧 = Automatically fixable
Rules that catch general CSS syntax errors and invalid constructs.
| Rule | Description | 🔧 | Status |
|---|---|---|---|
| no-unknown-pseudo-class | Detect invalid pseudo-classes | Planned | |
| no-unknown-pseudo-element | Detect invalid pseudo-elements | Planned |
Rules specific to Tailwind CSS version management and migration.
| Rule | Description | 🔧 | Status |
|---|---|---|---|
| version-compatibility | Enforce compatibility with specific Tailwind CSS versions | Planned | |
| deprecated-features | Warn about deprecated Tailwind features | Planned | |
| migrate-imports | Convert Tailwind v3 imports to v4 syntax | 🔧 | Priority |
| migrate-directives | Update deprecated Tailwind directives | 🔧 | Priority |
| migrate-config-to-css | Guide migration from JS config to CSS @theme | Planned | |
| migrate-arbitrary-values | Update arbitrary value syntax between versions | 🔧 | Planned |
Rules that enforce documentation and maintainability.
| Rule | Description | 🔧 | Status |
|---|---|---|---|
| comment-word-disallowed-list | Disallow specified words in comments | Considering | |
| require-description-comments | Require explanatory comments for complex selectors | Considering | |
| tailwind-comment-directives | Validate Tailwind-specific comment directives | 🔧 | Considering |
General CSS formatting rules.
| Rule | Description | 🔧 | Status |
|---|---|---|---|
| indent | Enforce consistent indentation | 🔧 | Planned |
| brace-style | Enforce consistent brace placement | 🔧 | Planned |
| block-spacing | Enforce consistent spacing inside blocks | 🔧 | Planned |
| declaration-block-newline | Enforce line breaks within declaration blocks | 🔧 | Planned |
| rule-empty-line-before | Require or disallow empty lines before rules | 🔧 | Planned |
| property-sort-order | Enforce consistent property declaration order | 🔧 | Planned |
| at-rule-formatting | Format at-rules consistently | 🔧 | Planned |
| no-unnecessary-whitespace | Disallow unnecessary whitespace | 🔧 | Planned |
| property-formatting | Format property declarations consistently | 🔧 | Planned |
| selector-formatting | Format selectors consistently | 🔧 | Planned |
| value-formatting | Format property values consistently | 🔧 | Planned |
| media-query-formatting | Format media queries consistently | 🔧 | Planned |
Rules for Tailwind CSS v4 specific constructs.
| Rule | Description | 🔧 | Status |
|---|---|---|---|
| at-apply-formatting | Format @apply directives consistently | 🔧 | Planned |
| theme-formatting | Format @theme blocks consistently | 🔧 | Planned |
| enforce-class-order | Enforce consistent Tailwind utility class ordering | 🔧 | Priority |
Rules for consistent comment styles.
| Rule | Description | 🔧 | Status |
|---|---|---|---|
| comment-formatting | Format comments consistently | 🔧 | Considering |
| comment-style | Enforce consistent comment syntax | 🔧 | Considering |
| comment-empty-line-before | Require or disallow empty lines before comments | 🔧 | Considering |
| comment-capitalization | Enforce consistent comment capitalization | 🔧 | Considering |
| comment-length | Enforce maximum comment line length | 🔧 | Considering |
Status Legend:
- Priority: High priority, will be implemented next
- Planned: Scheduled for implementation
- Considering: Under consideration, may be implemented
This plugin extends @eslint/css with Tailwind CSS v4 syntax support:
- ✅ Directives:
@theme,@import,@apply,@plugin,@utility,@variant,@custom-variant,@source,@config,@reference,@tailwind(v3 legacy) - ✅ Functions:
theme() - ✅ Arbitrary Values:
[value]syntax - ✅ Modifiers:
hover:,focus:,sm:,lg:,inert:,target:,open:,starting:,popover-open:,not-*:,in-*:,[&:state]: - ✅ Stacked Variants:
dark:hover:text-white - ✅ Custom Variants: Created with
@custom-variant
Full TypeScript support with exported types:
import type { TailwindcssRules } from '@poupe/eslint-plugin-tailwindcss';The plugin exports a parser API for advanced use cases and custom rule development:
import {
// CSS context utilities
getCSSContext,
isCSSContext,
type CSSContextInfo,
type CSSRuleContext,
// CSS types from @eslint/css
CSSSourceCode,
type CSSLanguageOptions,
// Tailwind v4 syntax configuration
tailwindV4Syntax,
} from '@poupe/eslint-plugin-tailwindcss/parser';Validates whether an ESLint rule context is processing CSS content (including
Vue SFC <style> blocks).
export function getCSSContext<T extends RuleContextTypeOptions>(
context: RuleContext<T>,
): CSSContextInfo | undefinedReturns CSSContextInfo if the context contains CSS content, or undefined otherwise.
Quick check if a context contains CSS content.
export function isCSSContext(context: unknown): booleanInformation about CSS context:
interface CSSContextInfo {
contextType: 'css-file' | 'vue-style' // Type of CSS context
isVueFile: boolean // Whether it's a Vue SFC
sourceCode: unknown // The source code object
context: CSSRuleContext // Properly typed CSS rule context
getCSSSourceCode(): CSSSourceCode | undefined // Get CSSSourceCode instance
}import { getCSSContext } from '@poupe/eslint-plugin-tailwindcss/parser';
export const myCustomRule = {
meta: { /* ... */ },
create(context) {
// Check if we're in CSS context
const cssInfo = getCSSContext(context);
if (!cssInfo) {
return {}; // Not CSS content
}
// Now we have properly typed CSS context
const { context: cssContext, getCSSSourceCode } = cssInfo;
// Check if we have a CSSSourceCode instance
const cssSourceCode = getCSSSourceCode();
if (cssSourceCode) {
// Can use CSSSourceCode-specific APIs
}
return {
StyleSheet(node) {
// Rule implementation with typed context
cssContext.report({
node,
message: 'Example error'
});
}
};
}
};The CSSSourceCode class from @eslint/css provides methods for working with CSS
source code:
const cssSourceCode = cssInfo.getCSSSourceCode();
if (cssSourceCode) {
// Access CSS-specific source code methods
const text = cssSourceCode.getText(node);
const lines = cssSourceCode.getLines();
}Type definition for CSS language configuration options from @eslint/css:
interface CSSLanguageOptions {
tolerant?: boolean;
customSyntax?: Partial<SyntaxConfig> | SyntaxExtensionCallback;
}The tailwindV4Syntax export is a SyntaxExtensionCallback that wraps
tailwind-csstree's tailwind4 callback and adds
@tailwind for v3 legacy compatibility:
import { tailwindV4Syntax } from '@poupe/eslint-plugin-tailwindcss/parser';
// Use in a custom ESLint config:
{
languageOptions: {
customSyntax: tailwindV4Syntax,
},
}This can be used when extending @eslint/css parsers or creating custom CSS linting tools.
// eslint.config.mjs
import tailwindcss from '@poupe/eslint-plugin-tailwindcss';
import tseslint from 'typescript-eslint';
export default [
...tseslint.configs.recommended,
tailwindcss.configs.recommended,
];// eslint.config.mjs
import tailwindcss from '@poupe/eslint-plugin-tailwindcss';
import vue from 'eslint-plugin-vue';
export default [
...vue.configs['flat/recommended'],
tailwindcss.configs.recommended,
];The preset file glob (**/*.?(post)css) covers .css and .postcss
files. No additional configuration is needed:
// @ts-check
// eslint.config.mjs
import tailwindcss from '@poupe/eslint-plugin-tailwindcss';
export default [
tailwindcss.configs.strict,
];Use both plugins to get css/* rules alongside tailwindcss/* rules.
The Tailwind preset is self-contained; @eslint/css needs its own
config entry with plugin registration and language:
// eslint.config.mjs
import css from '@eslint/css';
import tailwindcss from '@poupe/eslint-plugin-tailwindcss';
export default [
// @eslint/css — general CSS rules (css/css language)
{
files: ['**/*.css'],
plugins: { css },
language: 'css/css',
rules: {
...css.configs.recommended.rules,
// Tailwind directives are valid — handled by tailwindcss/css
'css/no-invalid-at-rules': 'off',
},
},
// Tailwind CSS — Tailwind-specific rules (tailwindcss/css language)
tailwindcss.configs.recommended,
];Make sure you're using ESLint flat config format (ESLint 9+):
// @ts-check
// ✅ Correct - eslint.config.mjs (ESM format)
export default [
tailwindcss.configs.recommended,
];
// ❌ Wrong - .eslintrc.js (legacy format)
module.exports = {
extends: ['@poupe/eslint-plugin-tailwindcss'],
};Use a preset config directly — it already targets **/*.?(post)css:
export default [
tailwindcss.configs.recommended,
];Full error:
TypeError: Key "languageOptions": Expected an object value for
'customSyntax' option.
This means an older @eslint/css (< 1.0) was resolved against the
plugin. Versions before 1.0 only accept a plain object for
customSyntax, while this plugin passes a SyntaxExtensionCallback
(supported since @eslint/css 1.0). Although the plugin declares
@eslint/css ^1.1.0 as a peer dependency, pnpm only emits a warning
when another package in the tree pins an older version, so the
mismatch can slip through at install time and only surface when ESLint
loads the config.
To fix it, ensure @eslint/css ^1.1.0 wins resolution in your project:
- If you use
@poupe/eslint-config, upgrade to~0.9.1or later — earlier versions pinned@eslint/css 0.14.x. - Otherwise, audit your lockfile (
pnpm why @eslint/css) for the package forcing the older version and upgrade or override it.
Contributions are welcome! Please read our contributing guidelines first.
# Install dependencies
pnpm install
# Run tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Run tests with coverage
pnpm test:coverage
# Build
pnpm build
# Lint and auto-fix
pnpm lint
# Type checking
pnpm type-check
# Run all pre-commit checks
pnpm precommitTests are written using Vitest and ESLint's RuleTester. Each rule should have comprehensive test coverage including:
- Valid code examples
- Invalid code examples with expected errors
- Auto-fix scenarios (where applicable)
- Edge cases and CSS parsing quirks
Example test structure:
import { RuleTester } from 'eslint';
import css from '@eslint/css';
import { ruleName } from '../../rules/rule-name';
const ruleTester = new RuleTester({
language: 'css/css',
plugins: { css },
});
describe('rule-name', () => {
ruleTester.run('tailwindcss/rule-name', ruleName, {
valid: [
// Valid test cases
],
invalid: [
// Invalid test cases with expected errors
],
});
});MIT © Apptly Software Ltd