eslint-plugin-better-tailwindcss

repository·main·Indexed 21 days ago

https://github.com/schoero/eslint-plugin-better-tailwindcss

An ESLint and Oxlint plugin for Tailwind CSS that provides formatting and linting rules. It automates class sorting, grouping, and line-breaking to improve readability and enforces best practices through stylistic and correctness rules. Compatible with React, Solid.js, Qwik, Svelte, Vue, Astro, Angular, HTML, JavaScript, and TypeScript, with built-in support for utility libraries like tailwind-merge, cva, and clsx.

Tokens
33.3K
Snippets
87
Records
134
Agent score
73%

What's inside eslint-plugin-better-tailwindcss

  1. Overview of eslint-plugin-better-tailwindcss

    main

    eslint-plugin-better-tailwindcss is an ESLint/Oxlint plugin designed to improve Tailwind CSS code quality. It provides two types of rules:

    1. Formatting rules: Improve readability by automatically breaking long Tailwind class strings into multiple lines and sorting/grouping them logically.
    2. Linting rules: Enforce best practices and catch potential issues to ensure valid Tailwind CSS usage.

    The plugin is compatible with various frameworks and environments, including React, Solid.js, Qwik, Svelte, Vue, Astro, Angular, HTML, and plain JavaScript or TypeScript.

  2. Use Selector Matchers to extract strings

    main

    Matchers define how to find strings within the selected location.

    • strings: Matches all direct string literals (excludes object keys/values).
    • objectKeys: Matches object keys. Can be narrowed with a path regex.
    • objectValues: Matches object values. Can be narrowed with a path regex.
    • anonymousFunctionReturn: Matches values returned from anonymous functions. Requires a nested match array of other matchers.
  3. Narrow object matching with the `path` option

    main

    When using objectKeys or objectValues matchers, use the path property to target specific nested locations using a regex. The path follows standard object notation:

    • Dot notation: root.nested.values
    • Square brackets for arrays: values[0]
    • Quoted brackets for special characters: root["some-key"]
    // Example: Match only values in compoundVariants[n].class or compoundVariants[n].className
    {
      "type": "objectValues",
      "path": "^compoundVariants\[\\d+\\]\.(?:className|class)$"
    }
  4. Configure custom selectors for Tailwind linting

    main

    To prevent false positives, the plugin requires selectors to identify which strings contain Tailwind classes. You can provide an array of selectors, where each selector targets a specific source location (attribute, callee, variable, or tag) and uses match rules to extract strings.

    Matching Rules:

    • Names (like name or path) are treated as regular expressions.
    • The regex must match the whole name (not a substring).
    • Reserved regex characters must be escaped.
  5. Supported utility libraries

    main

    The plugin automatically detects and lints Tailwind classes used within several popular utility libraries:

    • tailwind-merge: twMerge, twJoin
    • class-variance-authority: cva
    • tailwind-variants: tv
    • shadcn: cn
    • classcat: cc
    • class-list-builder: clb
    • clsx: clsx
    • cnbuilder: cnb
    • classnames-template-literals: ctl
    • obj-str: objstr
    • react-twc: twc, twx

    If a utility is not supported by default, you can customize the configuration via attributes, callees, variables, or tags in the advanced configuration settings.

  6. Use the enforce-shorthand-classes rule

    main

    The enforce-shorthand-classes rule identifies instances where multiple longhand Tailwind CSS classes can be replaced with a single shorthand class. This helps improve code readability and reduce bundle size.

    NOTE

    This rule may conflict with enforce-canonical-classes. It is recommended to use only one of these rules to avoid conflicting automatic fixes.

    // ❌ BAD: using separate padding classes
    <div class="pt-4 pr-4 pb-4 pl-4" />;
    
    // ✅ GOOD: using shorthand padding class
    <div class="p-4" />;
    
    // ❌ BAD: using separate width and height classes
    <div class="w-4 h-4" />;
    
    // ✅ GOOD: using shorthand size class
    <div class="size-4" />;
  7. Setup eslint-plugin-better-tailwindcss for Astro with Legacy Config

    main

    To lint Tailwind CSS classes in Astro files using the legacy ESLint configuration format (.eslintrc.json), install astro-eslint-parser and optionally @typescript-eslint/parser. Configure the top-level parser to astro-eslint-parser and use parserOptions.parser to specify the TypeScript parser if needed. Ensure the settings['better-tailwindcss'] object contains the correct Tailwind configuration path.

    // .eslintrc.json
    
    {
      // enable all recommended rules
      "extends": [
        "plugin:better-tailwindcss/legacy-recommended"
      ],
    
      // if needed, override rules to configure them individually
      // "rules": {
      //   "better-tailwindcss/enforce-consistent-line-wrapping": ["warn", { "printWidth": 100 }],
      // },
    
      "settings": {
        "better-tailwindcss": {
          // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)
          "entryPoint": "src/global.css",
          // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)
          "tailwindConfig": "tailwind.config.js"
        }
      },
    
      "parser": "astro-eslint-parser",
    
      "parserOptions": {
        "parser": "@typescript-eslint/parser"
      }
    }
  8. Use the no-concatenated-classes rule to prevent CSS purging

    main

    The no-concatenated-classes rule disallows the use of concatenated or interpolated Tailwind CSS classes within class strings.

    Tailwind's class detection mechanism relies on finding complete, static class names in your source files. When class names are built dynamically (e.g., using string concatenation or template literal interpolation), Tailwind may fail to detect them, causing those styles to be purged from the final CSS output. To ensure styles are applied correctly, always use fully static class names.

    // ❌ BAD: class name built dynamically via concatenation
    <div className={"bg-" + color} />;
    
    // ❌ BAD: class name built dynamically via interpolation
    <div className={`bg-${color}`} />;
    
    // ✅ GOOD: class name is fully static and detectable
    <div className="bg-red-500 text-white" />;
  9. Setup ESLint for CSS files with Tailwind CSS @apply

    main

    To lint Tailwind CSS classes within CSS files (specifically those using @apply directives), you must use the ESLint flat config format. This setup requires the @eslint/css plugin and the tailwind-csstree custom syntax to parse the CSS correctly.

    Prerequisites

    Install the necessary dependencies:

    npm i -D @eslint/css tailwind-csstree

    Configuration Requirements

    1. Install and configure the @eslint/css plugin.
    2. Install and configure the tailwind-csstree custom syntax.
    3. Add eslint-plugin-better-tailwindcss to your configuration.
    4. Configure the settings object with the appropriate Tailwind CSS configuration paths (see Configure Tailwind CSS paths).

    Note: Legacy ESLint configuration formats are not supported for CSS files because the @eslint/css plugin requires the Flat Config format.

  10. Extend default selectors in eslint-plugin-better-tailwindcss

    main

    The plugin provides a set of default selectors that determine how rules behave when checking your code. If you want to add custom selectors (like custom tags, callees, attributes, or variables) without losing the built-in ones, you should import getDefaultSelectors from eslint-plugin-better-tailwindcss/defaults and spread them into your rule configuration.

    import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
    import { getDefaultSelectors } from "eslint-plugin-better-tailwindcss/defaults";
    import { MatcherType, SelectorKind } from "eslint-plugin-better-tailwindcss/types";
    
    export default [
      {
        plugins: {
          "better-tailwindcss": eslintPluginBetterTailwindcss
        },
        rules: {
          "better-tailwindcss/enforce-consistent-class-order": ["warn", {
            selectors: [
              ...getDefaultSelectors(),
              // custom tag
              {
                kind: SelectorKind.Tag,
                match: [
                  {
                    type: MatcherType.String
                  }
                ],
                name: "^myTag$"
              },
              // custom callee
              {
                kind: SelectorKind.Callee,
                match: [
                  {
                    type: MatcherType.String
                  }
                ],
                name: "^myFunction$"
              },
              // custom attribute
              {
                kind: SelectorKind.Attribute,
                match: [
                  {
                    type: MatcherType.String
                  }
                ],
                name: "^myAttribute$"
              },
              // custom variable
              {
                kind: SelectorKind.Variable,
                match: [
                  {
                    type: MatcherType.String
                  }
                ],
                name: "^myVariable$"
              }
            ]
          }]
        }
      }
    ];
  11. Configure the enforce-consistent-line-wrapping rule

    main

    The better-tailwindcss/enforce-consistent-line-wrapping rule enforces breaking Tailwind classes into multiple lines based on line length, class count, or grouping. You can configure how classes are wrapped, how they are indented, and how they are grouped by variants.

    // Example ESLint configuration
    module.exports = {
      rules: {
        'better-tailwindcss/enforce-consistent-line-wrapping': [
          'error',
          {
            printWidth: 80,
            classesPerLine: 0,
            group: 'newLine',
            preferSingleLine: false,
            indent: 2,
            lineBreakStyle: 'unix',
            strictness: 'strict',
          },
        ],
      },
    };