Skip to content

poupe-ui/eslint-plugin-tailwindcss

Repository files navigation

@poupe/eslint-plugin-tailwindcss

ESLint plugin for Tailwind CSS v4 with advanced linting rules.

Features

  • 🎯 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

Requirements

  • ESLint 9.30.0 or higher
  • Node.js 20.19.0 or higher
  • Flat config format (eslint.config.js)

Installation

# pnpm
pnpm add -D @poupe/eslint-plugin-tailwindcss

# npm
npm install -D @poupe/eslint-plugin-tailwindcss

# yarn
yarn add -D @poupe/eslint-plugin-tailwindcss

Note: 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.

Usage

Basic Setup

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.

Overriding Rules

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',
    },
  },
];

Available Configurations

base

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.

minimal

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

recommended (Default)

Balanced set of rules for most projects.

  • ✅ All rules from minimal (no-important relaxed to warning)
  • no-conflicting-utilities
  • ⚠️ no-arbitrary-value-overuse (warning)
  • ⚠️ prefer-theme-tokens (warning)

strict

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)

Complete Example

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

Possible Errors (Implemented)

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 🔧

Best Practices (Implemented)

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

Stylistic Issues (Implemented)

Rules that enforce code style and formatting conventions.

Rule Description 🔧
consistent-spacing Enforces consistent spacing around colons in CSS declarations 🔧

🔧 = Automatically fixable

Unimplemented Rules (Roadmap)

Possible Errors (Planned)

Core CSS Validation (Additional)

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

Best Practices (Planned)

Tailwind v4 Compatibility

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
Code Quality

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

Stylistic Issues (Planned)

CSS Formatting (@eslint/css parity)

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
Tailwind-Specific Formatting

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
Comment Formatting

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

Advanced Features

Custom Syntax Support

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

TypeScript Support

Full TypeScript support with exported types:

import type { TailwindcssRules } from '@poupe/eslint-plugin-tailwindcss';

Parser API

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';

CSS Context Utilities

getCSSContext(context)

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 | undefined

Returns CSSContextInfo if the context contains CSS content, or undefined otherwise.

isCSSContext(context)

Quick check if a context contains CSS content.

export function isCSSContext(context: unknown): boolean
CSSContextInfo

Information 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
}

Example: Custom Rule Using Parser API

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'
        });
      }
    };
  }
};

CSS Types from @eslint/css

CSSSourceCode

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();
}
CSSLanguageOptions

Type definition for CSS language configuration options from @eslint/css:

interface CSSLanguageOptions {
  tolerant?: boolean;
  customSyntax?: Partial<SyntaxConfig> | SyntaxExtensionCallback;
}

Tailwind v4 Syntax Configuration

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.

Configuration Examples

With TypeScript

// eslint.config.mjs
import tailwindcss from '@poupe/eslint-plugin-tailwindcss';
import tseslint from 'typescript-eslint';

export default [
  ...tseslint.configs.recommended,
  tailwindcss.configs.recommended,
];

Vue.js Project

// 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,
];

With PostCSS

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,
];

With @eslint/css

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,
];

Troubleshooting

ESLint can't find the plugin

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'],
};

Rules not applying to CSS files

Use a preset config directly — it already targets **/*.?(post)css:

export default [
  tailwindcss.configs.recommended,
];

TypeError: ... Expected an object value for 'customSyntax' option

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.1 or 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.

Contributing

Contributions are welcome! Please read our contributing guidelines first.

Development

# 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 precommit

Testing

Tests 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
    ],
  });
});

License

MIT © Apptly Software Ltd

About

ESLint plugin for Tailwind CSS v4 — syntax validation, best practices, and auto-fix

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors