Skip to content

v1.14.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 21:29
feeaa45

This release checks classes against your design system as a whole. Two new rules report what no
class-by-class check could: no-default-palette, a Tailwind palette color in a project with colors
of its own, and no-dynamic-classes, a class built at runtime that Tailwind never generates. An
experimental third, no-borrowed-component-styles, reports a plain element that rebuilds one of
your components. Messages name the fix — the theme colors to use, the closest scale steps — and a
misspelt setting is reported instead of silently ignored. Rule options and settings apply per file,
as oxlint's overrides and nested configs intend. A Claude Code plugin and an agent skill bring all
of it to AI coding agents. The results are measured: the new
Benchmark runs this plugin, @shadcn/lint and
eslint-plugin-better-tailwindcss over shadcn/ui's app and judges them with Tailwind itself.

Before you upgrade

  • One cold start per entry point. The design-system cache key changed, so the first run after
    upgrading precomputes each entry point again. That takes seconds, not minutes. Cache files unused
    for 30 days are now removed.

  • Tailwind CSS v4.1.15 or newer, and Node.js ^20.19.0 || >=22.12.0. Tailwind 4.1.0–4.1.14
    never worked: they crashed. They now get a clear designSystemUnavailable.

  • Your .oxlintrc.json runs only the rules it lists, so the new rules are opt-in. The
    recommended config, if you copy it, now includes no-dynamic-classes as error.

  • Reports that can appear with the same config:

    • a misspelt or mistyped setting (invalidSetting, once per file);
    • classes in strings joined with +;
    • max-class-count on long template literals;
    • rule options from overrides and nested configs, which now apply to their files;
    • consistent-variant-order, if you ran --fix with an entry point: it rewrites those chains
      back to outermost-first once.
  • Reports that go away:

    • no-dark-without-light on dark-only classes that set no colour;
    • unknown-class reports of classes defined in nested stylesheets, up to four @imports deep, and
      of the static text glued to a ${};
    • wrong typo suggestions on short classes.
  • New message texts, if you match them in scripts:

    • no-hardcoded-colors (noHardcodedTokens) names your theme's colors;
    • no-arbitrary-value (noArbitraryOptions / noArbitraryTheme) names the closest steps or the
      file to add a token to;
    • no-dark-without-light (missingBaseProperty) names the property.

    All three need an entry point; without one, the messages are unchanged.

Features

  • A Claude Code plugin. /plugin marketplace add sergioazoc/oxlint-tailwindcss, then
    /plugin install oxlint-tailwindcss@oxlint-tailwindcss. It starts the project's own
    oxlint --lsp as a language server, so Claude sees the diagnostics on the files it opens and
    edits. It also checks the files Claude edited before it finishes: when oxlint-tailwindcss reports
    errors in them, Claude is sent back once with the list. Warnings and files Claude didn't touch
    don't count. It bundles the agent skill, and runs the project's own oxlint and config. See
    AI coding agents.

  • New rule, experimental: no-borrowed-component-styles. A <button> that copies Button's
    classes — or rewrites them from memory — looks right today and drifts tomorrow: it doesn't follow
    Button's changes, and it never had its focus ring, disabled state or variants. Point the rule at
    your components ("components": ["src/components/ui"]) and it reports a native element whose
    classes rebuild one of them, naming the component and its file (borrowedStyles: "
    rebuilds Button: 9 of its 9 style declarations match. Use Button from components/ui/button.tsx
    instead…"). It reads each component's cva() / tv() styles and root className without running
    them, and compares what the classes do, not how they're written: px-4 and pl-4 pr-4 are the
    same style, layout and variant classes don't count, a different color is a different style, and
    text styles alone never match. DS-dependent, no autofix, off in the recommended config.
    Experimental: its heuristics may change in a minor release. On shadcn/ui apps/v4: 1 report, a
    checkbox drawn by hand inside a command-menu item.

  • no-hardcoded-colors names your theme's colors. With an entry point, and a theme that defines
    colors of its own, the message lists them —
    "bg-[#ff6600]" uses a hardcoded color value. Use one of your theme colors instead: brand, brand-light.
    — so the replacement is in the report. The design system is loaded only once there is a color to
    report; without an entry point, or on stock Tailwind, the message is unchanged. The rule becomes
    DS-optional and accepts a per-rule entryPoint; it never reports a missing design system. Same
    reports as before, only the text changes.

  • New rule: no-default-palette. In a project whose theme defines its own colors, a color from
    Tailwind's default palette — bg-red-500, text-gray-900, bg-white — is usually a
    design-system leak: it doesn't follow the theme, and bg-white stays white in dark mode. The rule
    reports every class that reads a palette color (with variants, opacity modifiers, and arbitrary
    values that read a palette variable), and names the project's colors in the message. A palette
    color the theme redefines is the project's own; with no colors of its own, the rule reports
    nothing. allow takes names or * prefixes (["white", "gray-*"]). DS-dependent, no autofix,
    off in the recommended config. On shadcn/ui apps/v4: 144 reports, 18 of them white / black.

  • A misspelt or mistyped setting is reported. oxlint doesn't check settings, and the plugin
    read each key it knew and nothing else, so rootfontsize, atributes or
    "attributes": "className" (read letter by letter) changed nothing — or the wrong thing —
    silently. Now the first rule to lint each file reports what's wrong with settings.tailwindcss on
    its first line, once per file: an unknown key (with the setting you likely meant), a value of the
    wrong type, a pattern that isn't a regular expression, or a calleeExtractors kind that doesn't
    exist (invalidSetting: "Check settings.tailwindcss: "rootfontsize" is not a setting (did you
    mean "rootFontSize"?)"). Valid settings report nothing new.

  • New rule: no-dynamic-classes. Tailwind generates CSS only for class names written out in
    full, so a class built at runtime — `bg-${color}-500`, `w-[${width}px]`,
    `hover:${cls}` — gets no CSS and the element silently goes unstyled. The rule reports each
    such class once, when the text glued to the ${} starts with a Tailwind utility or variant (your
    project's own ones too, with an entry point); `${base} p-4` and `icon-${name}` are
    fine. No autofix. In the recommended README config as error. On shadcn/ui apps/v4: 0 reports.

  • enforce-canonical can tell you when a canonical form means something else in your project:
    reportNonEquivalent: true
    (off by default). shadcn/ui ships data-disabled:, data-open:… as
    custom variants built on :where(…), so data-[disabled]:opacity-50 — which stock Tailwind
    rewrites to data-disabled:opacity-50 — matches different elements there. Since #78 the rule
    silently left such classes alone; with the option it reports them, naming both variants, without a
    fix. The check compares against stock Tailwind on the same engine, so a selector difference stock
    Tailwind has too (has-[[data-slot=x]]: vs has-data-[slot=x]:) is not reported. On shadcn/ui
    apps/v4: 23 reports, all data-[disabled]:.

  • no-arbitrary-value names the fix. With an entry point, the report lists the closest steps
    and tokens of your theme with their values, and the file to add a token to: "Closest in your
    theme: w-50.5 (202px) or w-51 (204px). If none fits, add a token to src/styles.css." An exact
    match is offered alone (h-[26px] → h-6.5); a value that isn't a length gets the file. New
    messageIds noArbitraryOptions / noArbitraryTheme; without an entry point the message is
    unchanged. On shadcn/ui apps/v4, 292 of the 752 reports now name exact options.

  • no-arbitrary-value can allow runtime CSS variables:
    allowVariables: 'none' | 'runtime' | 'all'
    (default 'none', so nothing changes unless you
    opt in). A value that is nothing but a variable — w-(--sidebar-width),
    h-[var(--radix-select-trigger-height)] — often reads something JavaScript or an inline style
    sets, with no token to use instead. 'runtime' allows the variables no CSS in your design system
    defines and still reports the ones it does (--primary has a named utility); 'all' allows every
    pure variable reference. w-[calc(var(--x)*2)] is still an arbitrary value. The rule becomes
    DS-optional: only 'runtime' reads the design system. On shadcn/ui apps/v4, 167 of the rule's
    752 reports are pure variable references.

Bug fixes

  • Classes and variables from nested stylesheets are known. The plugin read the classes and
    custom properties your own CSS defines only in the entry point and the files it imports directly,
    so a class in a stylesheet imported by an imported stylesheet — even in @layer components — was
    reported as unknown, and its variables as undefined. It now follows your local @imports up to
    four levels, the depth whose edits already invalidate the cache; a package's stylesheet is still
    read without its own imports, so Tailwind's theme stays the engine's.
  • Class strings built with + are linted. The extractor skipped concatenations, so no rule
    checked the classes in "flex p-4 " + extra or "flex " + (active ? "itms-center" : ""), and
    no-dynamic-classes couldn't report "bg-" + color. Each string operand is now read like a
    template-literal part, glued to its neighbour only where the source is — "flex " + "p-4" holds
    two complete classes, "bg-" + color one runtime class — so complete classes get every rule and
    their fixes, glued fragments are left byte for byte, and no-dynamic-classes reports the runtime
    class written as a template ("bg-" + color + "-500" → bg-${color}-500), including one split
    over two strings ("bg-" + "red-500"), which Tailwind reads as two. New reports in projects
    that build classes with +. On shadcn/ui apps/v4: none (it doesn't).
  • consistent-variant-order writes chains outermost first, with or without an entry point.
    Without an entry point the rule wanted dark:hover: and sm:hover: — as its docs, Tailwind's
    docs and shadcn/ui write them — but with one it followed the order Tailwind registers variants in
    and wanted the opposite, hover:dark: and hover:sm:, so configuring an entry point flipped the
    autofix. Both now use one order: page conditions (breakpoints, dark, supports-*, container
    queries) → group-* / peer-* → what the element is (aria-*, data-*, has-*) → how it's
    used (hover, focus) → form state → position (first, last), with pseudo-elements innermost
    as before. The design system only says what the project's own variants are — a pseudo-element, a
    barrier, or a --breakpoint-* — and a @custom-variant the rule can't place is left where it is,
    as is everything across it. Without an entry point, aria-* / data-* / has-* now go before
    hover / focus / first (they used to sort last). If you ran --fix with an entry point,
    the rule rewrites those chains back once.
    On shadcn/ui apps/v4: 58 reports → 8.
  • no-unknown-classes keeps named group/peer markers valid when a custom variant names
    .group.
    With a @custom-variant whose selector names .group or .peer in the entry point
    (e.g. next-group-hover (&:is(:where(.group):has(~ .group:hover) *))), group/row and
    peer/field were reported as unknown. The selector scan put group/peer into the component
    set, and the marker check refused any component base; it now refuses only component classes
    Tailwind does not generate itself
    (#165, reported and fixed by
    @followdarko).
  • Tailwind 4.1.0–4.1.14 are rejected with a clear message instead of a raw engine error. The
    precompute calls ds.canonicalizeCandidates, which Tailwind only ships from 4.1.15, but the
    version guard's floor was 4.1.0 — so those releases crashed with
    ds.canonicalizeCandidates is not a function behind a hint to check the CSS for syntax errors.
    The guard now names the real floor, v4.1.15. Those versions never worked; only the message
    changes. CI pins the floor exactly and asserts that 4.1.14 is rejected by the guard.
  • Rule options and settings now apply per file. oxlint lints many files with one rule instance,
    and the plugin compiled options and settings on the first file and kept them for the whole run. So
    a rule's options from an overrides block or a nested .oxlintrc.json were ignored (or leaked
    into other files), and a nested config's attributes, callees, tags, variablePatterns,
    calleeExtractors, rootFontSize and debug applied only if a file of that package happened to
    be linted first — which, with several threads, changed from run to run. Each file now gets its own
    config's values; entryPoint was already per file. Files of one config still share one compiled
    config, so this costs nothing measurable.
  • no-dark-without-light only asks for a light-mode base for colours. A dark-only filter,
    display, border width or opacity — dark:brightness-[0.2] dark:grayscale on an image,
    dark:hidden on a decoration — was reported as missing a base nobody should add; light mode keeps
    the element's normal rendering on purpose. With an entry point, a variant class is checked when
    the CSS it emits sets a colour (a colour property, a --color-* variable or a colour literal);
    without one, the families that are never colour are skipped. dark:text-white on its own is still
    reported. With an entry point the message now names the property (missingBaseProperty: "sets
    color only under the dark variant…"). On shadcn/ui apps/v4: 11 reports → 1
    (dark:text-foreground).
  • Autofixes no longer split a class built with ${}. In `bg-${c}-500 p-4 p-4` the static
    text glued to the expression (bg-, -500) is part of one runtime class, but rules saw it as
    classes of their own, and fixes re-inserted a space at the ${} boundary: no-duplicate-classes
    turned that string into `bg-${c} -500 p-4`, and enforce-logical turned
    `ml-2 text-${c}` into `ms-2 text- ${c}`. The same happened in
    enforce-consistent-important-position, enforce-consistent-variable-syntax,
    enforce-sort-order and consistent-variant-order (which could also move a fragment away from
    its expression), enforce-shorthand, enforce-canonical, no-deprecated-classes,
    enforce-negative-arbitrary-values, no-unnecessary-arbitrary-value, and in the suggestions of
    no-unknown-classes, prefer-scale-token and prefer-theme-tokens. Glued fragments are now left
    out of what these rules check and rewrite, so the dynamic class is kept byte for byte and
    no-unknown-classes no longer reports bg- or -500 as unknown classes.
    no-unnecessary-whitespace, enforce-consistent-line-wrapping and max-class-count still see
    the text as written.
  • no-unknown-classes offers fewer wrong quick-fixes on short classes. A suggestion used to be
    any known class within 3 edits, so a CSS-less marker such as line was offered inline (a layout
    change), and w-[] was offered w-0. The allowance now scales with length (one edit per three
    characters, from 1 to 3) for utilities and variants alike, and swapping two adjacent letters
    counts as one edit, so flxe → flex and opne: → open: are still suggested. Such classes are
    still reported; only the suggestion goes away.
  • max-class-count counts a template literal as one class string. Each part between ${} was
    counted on its own, so `a b c ${x} d e f` read as two strings of 3 and never reached a max
    of 4. The parts are now added up and reported once, a class glued to an expression
    (bg-${tone}-500) counts once, and a bare ${expr} counts as none. This can add reports on
    long templates. Separate cn() arguments and cva()/tv() values are still counted one by one,
    since they are often conditional or alternatives; the docs used to say the count was per element
    or per call, which was never the case, and now describe it as it is. The README example printed 20
    classes under a "(21)" diagnostic; it now has 21.
  • The design-system cache notices local plugins and package upgrades. Its key covered the CSS
    entry and its local @imports, but not a local @plugin / @config file or the version of a
    package the CSS imports or loads as a plugin (tw-animate-css, @tailwindcss/typography).
    Editing the plugin or upgrading the package kept serving the old class list, so a class that no
    longer existed stopped being reported until the cache was cleared. Both are part of the key now.
    The new key means one cold precompute per entry point after upgrading.
  • debug prints one line per file. The file → entry point line came once per
    design-system-dependent rule, so with four such rules on, every file was listed four times.
  • Tailwind insiders builds are no longer rejected as "too old". 0.0.0-insiders.<sha> was read
    as version 0; it is now treated as an untested newer engine — a one-time notice, then linting as
    usual.

Performance

  • The disk cache no longer grows forever. Every change to a design system's CSS writes a new ~3
    MB precompute, and nothing removed the old ones — in a CI cache restored run after run they piled
    up. After each precompute, the plugin's own cache files unused for 30 days are removed; a cache
    hit refreshes a file's date — at most once a day, so a warm run still writes nothing — and what's
    in use never expires. Other files in the directory are left alone.
  • The precompute is about 1 s faster. It builds the map of arbitrary values to named classes
    (h-[auto] → h-auto) first thing instead of late: the same Tailwind call costs about five times
    as much once the canonical-forms phase has run (1.6 s instead of ~0.5 s on shadcn/ui's theme). The
    result is byte for byte the same on every test theme. With the change below, a cold start on
    shadcn/ui apps/v4 goes from 10.8 s to 7.9 s on one thread.
  • An engine without canonicalizeCandidates (Tailwind before 4.1.15, reachable only with
    allowUntestedEngine) no longer fails the whole precompute: the canonical and v3-rename maps stay
    empty and every other rule works.
  • A cold start is about 1.5 s shorter. When the design system has to be precomputed, the
    enforce-canonical worker now starts alongside it and builds Tailwind's canonicalization tables
    (~1 s, once per worker) in the meantime, instead of after it on the first class that needs it. On
    shadcn/ui apps/v4: 10.8 s → 9.2 s cold on one thread, with identical diagnostics; warm runs are
    unchanged.
  • debug says where a cold start's time goes. Each design-system load now logs whether it came
    from the disk cache or a fresh precompute, and how long hashing, the cache read, the precompute
    and building the in-memory cache took — for a precompute, phase by phase, timed by the precompute
    itself (precomputed in 3850 ms (load 20 ms, validate 402 ms, canonical 1416 ms, …)).
  • no-unknown-classes finds typo suggestions in about half the time. The edit-distance scan
    over every known class now stops as soon as a candidate can't beat the suggestion budget, and
    reuses its buffers instead of allocating per candidate. On shadcn/ui apps/v4, warm, the rule
    went from 502 ms to 277 ms, with identical diagnostics.

Documentation

  • The npm README is short. Installation, the full config, a generated table of every rule — what
    it checks, its recommended severity, its fix, whether it needs the design system — linked to its
    page, the files and frameworks it reads, and the requirements. The per-rule reference, the
    settings and the edge cases are on the docs site, where they are tested; the README's copy had
    drifted (it still said only first-level @imports were read). The package's description and
    keywords say what it does.
  • An agent skill. npx skills add sergioazoc/oxlint-tailwindcss --skill oxlint-tailwindcss
    installs skills/oxlint-tailwindcss/SKILL.md into an AI coding agent: how to set the plugin up —
    requirements, the entry point, the recommended config — and what to do about each diagnostic.
    Tests keep it in line with the docs: its config runs with the real oxlint, its requirements and
    rule names are the code's, and every page it links exists.
  • New pages: Benchmark and
    Compared with other plugins (EN and ES).

    oxlint-tailwindcss, @shadcn/lint and eslint-plugin-better-tailwindcss are run over shadcn/ui's
    apps/v4 and a set of labeled mistakes, and Tailwind itself judges the results: which mistakes
    each tool catches, its false positives, its speed, and whether its fixes change what a class
    string does. On that corpus, every class string --fix changed kept its style. Every number is
    generated from bench/results/latest.json, which a weekly job checks against a fresh run.
  • The warning and tip boxes on /setup and /monorepo render again (EN and ES). oxfmt's
    proseWrap had merged each container's opener, body and closer into one paragraph, so the page
    printed a literal ::: and the unclosed box wrapped everything below it. A new test keeps every
    container in the docs and READMEs well-formed.
  • Vue, Svelte and Astro coverage is stated as it is. The setup page said the plugin lints "every
    … Vue / Svelte file"; oxlint only passes plugins the <script> blocks (and Astro's frontmatter),
    so template and markup classes are not checked. A new
    Vue, Svelte & Astro page shows what is and
    isn't linted, and an end-to-end test with canaries keeps the docs honest when oxlint changes it.
  • /monorepo explains where options and settings can vary per package (overrides for options
    only, nested configs for both, Vite+ and editor caveats), and its Pattern B example now uses
    "extends": ["../../.oxlintrc.json"]: the string form it showed is rejected by oxlint
    (expected a sequence).
  • New Running in CI page (EN and ES): pull-request
    annotations with -f github, keeping the design-system precompute between jobs with
    actions/cache and OXLINT_TAILWINDCSS_CACHE_DIR, per-rule timings with --debug timings (which
    print nothing with -f github), and what to trust in a restored cache. A new e2e test holds the
    cache-directory behavior it describes.
  • /settings lists the environment variables — DEBUG, OXLINT_TAILWINDCSS_CACHE_DIR (which
    was undocumented) and OXLINT_TAILWINDCSS_WORKER_REQUEST_TIMEOUT — and its debug section lists
    what is actually logged; it promised "cache hits", which are never logged.
  • The v0 → v1 migration guide is accurate for today's versions. It pointed at the v0.x cache
    directory without saying the cache moved to a per-user directory in v1.3, and said every timeout
    was set by settings.tailwindcss.timeout; that setting only moves the precompute timeout.
  • The docs site stops publishing its internal files. Every rules/_extras/* fragment (48 pages,
    EN and ES) and the agent notes were served as pages of their own; they are excluded now, and their
    old URLs redirect to the rule page. A dead internal link fails the docs build, "Edit this page" on
    a rule page opens the file the page is generated from, and the Spanish site's theme labels
    (search, outline, pager, 404…) are in Spanish.
  • The docs are checked against the code. New tests hold the rule count and lists, each rule's
    defaults and design-system group, every documented option and setting, the environment variables,
    the default extractor lists and the version floors, plus page-by-page parity between English and
    Spanish. They caught four gaps, now fixed: no-unknown-classes didn't document its entryPoint
    option, /settings never named exclude, the README's full setup snippet left out
    prefer-scale-token, and a typo.
  • Every docs page has its own title and description, in its own language. All pages shared the
    site-wide English description — Spanish pages included — and rule pages were titled with the bare
    rule name. Each rule page now opens with a one-sentence definition (also its search description)
    and an "At a glance" table — autofix, editor suggestions, whether it needs the design system, its
    options — generated from the rule's metadata.
  • The docs site names itself to search engines. Google showed "Cloudflare" as the site name for
    our pages: the site had no name signal of its own, and the pages.dev root it lives under
    redirects to cloudflare.com. The home page now declares a WebSite (structured data) named
    oxlint-tailwindcss, matched by og:site_name, the home title and the npm homepage, which now
    points at the docs. Every page gains a canonical URL, English/Spanish alternates, link-preview
    tags and image, and a favicon; rule pages get breadcrumbs; the 404 page is kept out of the index,
    and <html lang> is en / es.
  • The docs site is easy to crawl and to feed to an AI assistant. A sitemap.xml with
    English/Spanish alternates, its own robots.txt (search and AI use welcome, via Content Signals),
    and llms.txt /
    llms-full.txt plus a markdown copy of
    every page (/setup.md, /rules/no-unknown-classes.md), each linked from its HTML page.
  • One recommended config. /setup's starter set and the README's config disagreed (different
    rules, no-duplicate-classes error in one and warn in the other). Both are now generated from
    a recommended severity in each rule's metadata, and so is the README's rule list by category.
    The four categories are Correctness, Modernization, Consistency and Design-system guardrails.
  • The oxfmt / Prettier interop claims hold, with the setting they were missing. The docs said
    the formatter and enforce-sort-order "agree byte-for-byte if they read the same CSS"; that also
    needs the formatter's functions (tailwindFunctions for Prettier) to list your class helpers —
    otherwise cn("p-4 flex") stays unsorted — and it covers only the strings the formatter formats.
    /interop now says who formats what, that the formatter also drops duplicates and collapses
    whitespace, and that oxlint --fix applies one fix per class string per run (it has no multi-pass
    fixing), with the snippets updated. A new end-to-end test with the real oxlint and oxfmt holds
    each of these.
  • Every example on the rule pages runs as a test, in English and Spanish. Each ✗ example must be
    reported by its rule and each ✓ example must not be, with the options it states, and the fixed
    result shown after → must be what oxlint --fix writes. That caught examples that didn't do
    what they said: the enforce-consistent-line-wrapping examples named their variables cardClass
    / buttonClass, which the plugin doesn't read by default (only className, classNames,
    classes and styles), so neither the ✗ nor the ✓ one was ever checked; the
    no-unnecessary-whitespace "tab" example wrote the escape \t, not a tab; no-unknown-classes
    called "hover:" + dynamicSuffix an "allowlisted" class, though nothing allowlisted it (the
    example now uses allowlist and ignorePrefixes); and the options several pages described in
    prose are now given as real config. Pages with an autofix now show the fixed line for every ✗
    example. The README's hero example is checked the same way.
  • New shadcn/ui and @shadcn/lint page (EN and
    ES):
    what each plugin reports when both run — restyled components, inline styles and palette
    colors are @shadcn/lint's; conflicts, duplicates, deprecated and non-canonical classes, dark
    variants without a light base and order are this plugin's; both report unknown classes and
    arbitrary values — a combined .oxlintrc.json that reports each problem once, what
    no-conflicting-classes finds in shadcn/ui's own select trigger, and the setup step by step for a
    person or an agent. The table is generated from data and checked against both plugins: this
    plugin's column in the test suite, @shadcn/lint's weekly against its latest release.
  • New
    migration guide from eslint-plugin-better-tailwindcss
    (EN and ES):
    each of its 15 rules and its settings mapped to this plugin's, with what differs in
    their options, the switch step by step with a before-and-after config, and the rules it doesn't
    have. Every row's example is reported by both of its rules, checked weekly against
    better-tailwindcss's latest release.
  • New FAQ (EN and ES), every answer checked against
    oxlint: ESLint and Tailwind v3, which rules run (oxlint's categories don't turn on a JS plugin's
    rules), the entry point, Vue and Svelte, why a class that works can be reported as unknown,
    turning a rule off for a line, --fix, formatters, @shadcn/lint, better-tailwindcss, CI and
    monorepos. The home page now says what the plugin is and what it catches, with a quick start; the
    sidebar groups the guides; and the docs build fails on a link to a heading that doesn't exist.
  • Every "Tailwind v4.1+" requirement now reads v4.1.15+.
  • The Node.js requirement matches oxlint's own range, ^20.19.0 || >=22.12.0, and engines
    says so: oxlint itself does not run on older Node 20 releases. CI now smoke-tests the documented
    floors — oxlint 1.43.0, and Node 20.19.0 — so both claims stay true.

Dependencies updated: oxlint / @oxlint/plugins 1.85, oxfmt 0.70, vitest 5.0.1, pnpm 12.6.

Dependencies

  • The runtime dependencies are unchanged: @tailwindcss/node and tailwindcss, loaded from your
    project. engines.node is now ^20.19.0 || >=22.12.0, the range oxlint itself requires.
  • Developed and tested with oxlint 1.85, oxfmt 0.70, vitest 5.0.2 and pnpm 12. CI adds Node 26 and
    smoke-tests the documented floors (oxlint 1.43.0, Node 20.19.0, Tailwind 4.1.15).