Skip to content

Migrating from eslint-plugin-tailwindcss ​

eslint-plugin-tailwindcss is the ESLint plugin for Tailwind CSS; its 4.x releases read Tailwind v4. oxlint-tailwindcss is built for oxlint and Tailwind v4. Each of eslint-plugin-tailwindcss 4.4.0's 9 rules has a counterpart here, and each pair reports the same example — the tables below are checked against it every week, with its latest release.

Rules ​

eslint-plugin-tailwindcssoxlint-tailwindcssNotes
classnames-orderenforce-sort-orderBoth sort by Tailwind's own class order, the one prettier-plugin-tailwindcss uses. mode: "strict" also groups classes by variant chain.
enforces-canonical-classnameenforce-canonicalBoth ask Tailwind for the canonical form, but this rule rewrites a class only when the two print the same CSS: [&>*]:flex-1 → *:flex-1 (another selector) and aspect-[16/9] → aspect-video (a theme variable) stay as written. The rest of what theirs folds in has its own rule here: v3 renames (bg-gradient-to-r) are no-deprecated-classes, bg-[var(--x)] → bg-(--x) is enforce-consistent-variable-syntax, !p-4 → p-4! is enforce-consistent-important-position, and -m-[5px] → m-[-5px] is enforce-negative-arbitrary-values.
enforces-negative-arbitrary-valuesenforce-negative-arbitrary-values
enforces-shorthandenforce-shorthandTheir overflow-hidden text-ellipsis whitespace-nowrap → truncate has no equivalent here. Four corners or sides become one class at once (rounded-tl-md rounded-tr-md rounded-br-md rounded-bl-md → rounded-md), where theirs merges them two by two.
important-modifier-suffixenforce-consistent-important-positionThe suffix (p-4!) is the default here too; position: "prefix" asks for !p-4 instead.
no-arbitrary-valueno-arbitrary-valueOff in both recommended configs. Here allow tolerates arbitrary values under the prefixes you list (grid-cols-), allowVariables a value that is only a CSS variable, and the message names the closest values in your theme.
no-contradicting-classnameno-conflicting-classesDecided from the CSS each class prints, and the message names the class that wins. It also reports a class another one makes redundant (reportRedundant), and allow exempts pairs. A variant that repeats its base class (flex hover:flex) is no-contradicting-variants.
no-custom-classnameno-unknown-classeswhitelist → allowlist for exact names and ignorePrefixes for families (custom-); a regular expression that is neither has no counterpart. The classes your CSS defines (@layer components, @utility) are always known, and a typo gets the closest class as a suggestion.
no-unnecessary-arbitrary-valueno-unnecessary-arbitrary-valueThis rule rewrites only when the named class prints the same CSS. Theirs also turns a value equal to a scale step or a theme value into its token (m-[8px] → m-2, w-[20rem] → w-xs): here that is prefer-scale-token, as a suggestion, since the token reads a variable your theme can change. z-[123] → z-123 is enforce-canonical.

Settings ​

Both plugins read settings.tailwindcss, with different keys:

eslint-plugin-tailwindcssoxlint-tailwindcssNotes
cssConfigPathentryPointRequired in both. A relative path resolves against the nearest oxlint config here, and against the nearest eslint.config.* or package.json in theirs. A glob → CSS mapping covers a monorepo.
attributesattributes, attributePatternsSetting theirs replaces its defaults (class, className, ngClass, @apply); here attributes adds to class and className, and attributePatterns matches names with regular expressions. ngClass and @apply have nothing to match: oxlint hands JS plugins neither Angular templates nor CSS.
functionscallees, tagsTheirs is one list for calls and tagged templates, and setting it replaces the defaults; callees and tags add to this plugin's. Of their defaults, classNames(…) and tw(…) as a call aren't read here: add them to callees if you use them.
parseKeyFunctions—The keys of an object passed to any callee are read as classes (cn({ "p-4": isOpen })), and its values aren't. Theirs reads the keys only in these functions (by default classnames, classNames, clsx) and the values elsewhere.
ignoredKeys—cva and tv configs are read by their structure: defaultVariants is skipped, compoundVariants and compoundSlots are read.
cacheMaxSize—The design system is cached on disk, keyed by the content of your CSS, and rebuilt when it changes: there is nothing to tune.
cacheMaxAge—Same as cacheMaxSize.

Switching, step by step ​

  1. Install oxlint and the plugin: pnpm add -D oxlint oxlint-tailwindcss.
  2. Create .oxlintrc.json with "jsPlugins": ["oxlint-tailwindcss"], and turn settings.tailwindcss into this plugin's keys with the settings table: cssConfigPath becomes entryPoint, which is required.
  3. Rename each tailwindcss/<rule> to the tailwindcss/<rule> of the rules table — both plugins use the tailwindcss prefix. Or start from the recommended config and add what you had on top.
  4. Remove eslint-plugin-tailwindcss from your ESLint config and uninstall it. Both plugins are named tailwindcss, so oxlint won't load the two side by side: it stops with Plugin name 'tailwindcss' is already registered.
  5. Run oxlint, then oxlint --fix for the autofixes.

A typical config, before:

js
// eslint.config.mjs
import tailwindcss from "eslint-plugin-tailwindcss";
import { defineConfig } from "eslint/config";

export default defineConfig([
  tailwindcss.configs.recommended,
  {
    settings: {
      tailwindcss: {
        cssConfigPath: "./src/styles.css",
        functions: ["cn", "cva", "classNames"],
      },
    },
    rules: {
      "tailwindcss/no-custom-classname": ["warn", { whitelist: ["swiper-.*", "legacy-card"] }],
      "tailwindcss/no-arbitrary-value": "warn",
    },
  },
]);

And after, with the rules the recommended config turned on:

jsonc
// .oxlintrc.json
{
  "jsPlugins": ["oxlint-tailwindcss"],
  "settings": {
    "tailwindcss": { "entryPoint": "src/styles.css", "callees": ["classNames"] }
  },
  "rules": {
    "tailwindcss/enforce-sort-order": "warn",
    "tailwindcss/enforce-canonical": "warn",
    "tailwindcss/enforce-negative-arbitrary-values": "warn",
    "tailwindcss/enforce-shorthand": "warn",
    "tailwindcss/enforce-consistent-important-position": "warn",
    "tailwindcss/no-unknown-classes": [
      "warn",
      { "ignorePrefixes": ["swiper-"], "allowlist": ["legacy-card"] }
    ],
    "tailwindcss/no-conflicting-classes": "error",
    "tailwindcss/no-unnecessary-arbitrary-value": "warn",
    "tailwindcss/no-arbitrary-value": "warn"
  }
}

What else changes ​

  • Vue and Svelte templates. Their recommended config lints .vue and .svelte files, templates included, through ESLint's parsers. In oxlint, JS plugins see only the <script> blocks of those files. See Vue, Svelte & Astro.
  • Classes in variables. This plugin also reads the strings assigned to className, classes or styles (variablePatterns), which theirs leaves alone, so expect reports there too.
  • Tailwind v3. This plugin reads Tailwind v4 CSS (v4.1.15 and later); a tailwind.config.js project needs to stay on eslint-plugin-tailwindcss 3.x.

Rules eslint-plugin-tailwindcss doesn't have ​

Worth a look once you've switched:

consistent-variant-order · enforce-consistent-line-wrapping · enforce-consistent-variable-syntax · enforce-logical · enforce-physical · max-class-count · no-borrowed-component-styles · no-contradicting-variants · no-dark-without-light · no-default-palette · no-deprecated-classes · no-duplicate-classes · no-dynamic-classes · no-hardcoded-colors · no-restricted-classes · no-unnecessary-whitespace · prefer-scale-token · prefer-theme-tokens

Released under the MIT License.