Repository navigation
v1.15.0
This release follows Tailwind 4.2's logical utilities and makes the plugin easy to share.
enforce-logical now converts the block axis — mt-4 → mbs-4, top-0 → inset-bs-0 — and, on
request, widths and heights; enforce-physical and enforce-shorthand follow it, and none of them
ever writes a class your Tailwind doesn't have. enforce-consistent-line-wrapping can wrap a JSX
className="…" in place, the layout eslint-plugin-better-tailwindcss writes. enforce-canonical
can report the canonical forms that also set a variable other utilities read. And
oxlint-tailwindcss/config brings the plugin to an oxlint.config.ts and to shared configs
published as packages.
Before you upgrade
- Reports that can appear with the same config:
enforce-logicalon top/bottom utilities (mt-*,pt-*,top-*,border-b,scroll-mt-*,
…): its defaultdirection: 'both'now has a block axis to cover. On shadcn/ui'sapps/v4it
goes from 600 to 1 160 reports.direction: 'inline'keeps the previous behaviour, and without
an entry point the new ones are only suggested, never autofixed;enforce-physicalon the logical block-axis utilities (mbs-*,inset-bs-*,border-be, …);enforce-shorthandon logical pairs (mbs-2 mbe-2→my-2,inset-s-0 inset-e-0→
inset-x-0).
- New fix output: with an entry point on Tailwind 4.2+,
enforce-logicalwritesinset-s-*/
inset-e-*forleft-*/right-*. It wrotestart-*/end-*, whichenforce-canonicalthen
rewrote to the same. - New messages:
enforce-logical's block-axis and size rewrites useuseLogicalWritingMode("…
so it follows the writing mode");enforce-canonicalwithreportNonEquivalentadds
nonEquivalentVariable. - A relative
entryPointin a.oxlintrc.jsonc,oxlint.config.tsoroxlint.config.mtsnow
resolves against that config's directory, not an outer config's or the working directory.
Features
-
enforce-logicalconverts the block axis, and sizes on request (#215). Tailwind 4.2 shipped
logical utilities for the block axis, and the rule now maps them:mt/mb→mbs/mbe,
pt/pb→pbs/pbe,scroll-mt… →scroll-mbs…,top/bottom→inset-bs/inset-be,
border-t/border-b→border-bs/border-be. They followdirection, whose default'both'
always meant both axes; until 4.2 there was no block-axis pair to map (see Before you upgrade).
The newsizing: truealso converts widths and heights (w-*→inline-*,h-*→block-*,
theirmin-/max-forms,size-*→inline-* block-*), off by default. These diagnostics read
… so it follows the writing mode(new messageIduseLogicalWritingMode), since top and bottom
differ from their logical forms only in vertical text, not in RTL.- On Tailwind 4.2+,
left/rightbecomeinset-s/inset-e— the spelling the design system
calls canonical, soenforce-canonicalno longer rewrites the rule's own output in a second
pass. On 4.1, or without an entry point, it is stillstart/end. - Never a dead class. With a design system the rule probes that your Tailwind has each 4.2
utility (an arbitrary value passes every validity check, somt-[3px]used to be good enough to
becomembs-[3px], which emits nothing on 4.1); without one it only suggests the block axis and
sizes. A size converts only when both classes declare the same value:w-*reads--width-*
first andinline-*only--container-*, sow-xsstays when your theme defines--width-xs.
Classes with no logical counterpart are left alone (w-dvh,h-dvw,max-w-prose,
max-w-screen-*,rounded-t-*). enforce-physicalconverts the block axis back, sizes withsizing: true, and never
touches a display utility (inline-flexis not an inline size).enforce-shorthandfolds the logical pairs:mbs-*+mbe-*→my-*,inset-s-*+
inset-e-*→inset-x-*,border-bs-*+border-be-*→border-y-*, and the four logical
sides. Combined withenforce-logical,mt-2 mb-2ends asmy-2whichever fix lands first.
- On Tailwind 4.2+,
-
enforce-consistent-line-wrappingcan wrap a JSX attribute string in place (#216). With the
newwrapStrings: "jsx"(default"never"),className="…"— or any attribute the plugin reads
— is wrapped into the same block as a template literal, the closing quote at the attribute line's
indentation. That is valid JSX as written: in a class attribute the newlines are plain whitespace,
so nothing is converted, and it's the layouteslint-plugin-better-tailwindcsswrites, so a
migrated codebase's multilineclassNamestrings get fixed again instead of only reported. Both
wrapLinesandclassesPerLineapply; JS strings (className={"…"},cn("…"), variables) still
only report, since they can't hold a raw newline. A CRLF file gets CRLF breaks. If oxfmt formats
withsortTailwindcss, set itspreserveWhitespace: true, or it collapses the block back. -
enforce-canonicalcan report a canonical form that also sets a variable other utilities read
(#217). Tailwind rewrites[line-height:var(--x)]toleading-(--x), butleading-(--x)also
sets--tw-leading, which everytext-*size reads for its line height, so next to one the two
can render differently. The rule left these as written and said nothing, so a CI lint never caught
them. WithreportNonEquivalent: truethey are now reported with the variable named and a
suggestion your editor can apply — never an autofix. 86 forms of the stock theme are of this kind:
[border-style:dashed]→border-dashed(--tw-border-style, read byborder-2),
[transition-duration:300ms]→duration-300(--tw-duration, read bytransition), and the
font-*,tracking-*,ease-*,outline-*andcontent-noneones. A project variant that also
adds such a variable is still reported as a variant. The option stays off by default, so nothing
changes without it. -
oxlint-tailwindcss/config: the plugin forextends, in anoxlint.config.tsor a shared
config (#218).tailwindcss()returns the plugin, registered by its absolute path, and the
recommended rules at their severities (tailwindcss({ recommended: false }): the plugin alone):export default defineConfig({ extends: [tailwindcss()], settings: { tailwindcss: { entryPoint: 'src/styles.css' } }, })
A preset published as a package can now bring the plugin as its own dependency: oxlint resolves a
plugin's bare name from where it runs, not from the config that lists it, so whether the preset's
dependency was found depended on the package manager and on how oxlint was started. The settings
stay in the project's config — oxlint never reads them from an extended config, sotailwindcss()
takes none and throws if given one. Needs oxlint 1.45.0+ on Node 22.18.0+, the floors of
oxlint.config.tswith objectextends; the plugin itself keeps its floors.
Bug fixes
- A relative
entryPointanchors to every config oxlint discovers. It was resolved against the
nearest.oxlintrc.jsononly, so a.oxlintrc.jsonc,oxlint.config.tsoroxlint.config.mts
was skipped and the entry point resolved against an outer config or the working directory. The
same goes forno-borrowed-component-styles'componentspaths. - Style objects are no longer read as classes. An object's string keys are read as conditional
classes (cn({ "bg-red-500": isError })), and the default^styles?$variable pattern reaches
the style objects of every framework, soconst style = { '--pane-width': w }reported
"--pane-width" is not a valid Tailwind class. A key is now skipped when it is a style
declaration: a custom property (React'sCSSProperties), a CSS property name (the kebab-case keys
of Solid, Vue, Qwik and Lit'sstyleMap), a-webkit-/-moz-/-ms-property, or a key
whose value is a string, number or template literal (Angular's'width.px': 240), since a class
map's value is a condition. Every other key is still a class.
Documentation
/monorepo's Vite+ warning is checked againstvp lintevery week, with the pinned and the
latest vite-plus: the plugin loads fromvite.config.ts, a nested.oxlintrc.jsonis ignored,
and theentryPointmapping still gives each package its own CSS (#221)./setupdocumentsoxlint.config.tsand shared configs, and every page that explains how a
relativeentryPointresolves names all four config files./migration/from-better-tailwindcssnames better-tailwindcss 4.9.0, and the pages that describe
what oxlint does were checked again on oxlint 1.87.
Dependencies
- Development only: oxlint and
@oxlint/plugins1.87, oxfmt 0.72 (which now formats Markdown
itself), vitest 5.0.3, pnpm 12.10.1 (#220). The runtime dependencies are unchanged.