Repository navigation
enforce-consistent-line-wrapping: opt-in fix for JSX attribute strings #216
Copy link
Copy link
Closed
Description
Summary
enforce-consistent-line-wrapping never fixes a string literal. The docs give the reason: a string literal can't safely span lines, and turning it into a multiline template is a judgment call. That holds for a JS string, but not for a JSX attribute string, which can span lines as it is:
<div
className="
flex items-center justify-between rounded-md bg-bg-elevated px-3 py-1
text-sm text-fg-secondary shadow-md
"
/>This is valid JSX, and the class attribute treats the newlines as ordinary whitespace. Nothing gets converted, so there's no template to decide on. I'd like to ask whether an opt-in fix for this case would be welcome. If so, I'm happy to implement it.
Why
- The plugin already reads this form correctly. Line measurement is per line (enforce-consistent-line-wrapping: printWidth is compared against total raw string length instead of the longest individual line #110), so an already-wrapped JSX string that fits reports nothing, and
no-unnecessary-whitespacepreserves intentional newlines (no-unnecessary-whitespace and enforce-consistent-line-wrapping contradict each other #14). Only the fix is missing: an over-widthclassName="…"getstooLongand has to be wrapped by hand. - It's what people migrating from better-tailwindcss have in their code.
eslint-plugin-better-tailwindcss'senforce-consistent-line-wrappingwraps JSX attribute strings in place, and its output is exactly the shape above. After migrating, one codebase I work on has 328 such multilineclassName="…"attributes. They lint clean, but new ones no longer get fixed. - The layout already exists. The block form
wrapLineswrites for a template (classes on indented lines, the closing delimiter at the base indent) is the same layout inside"…".
Proposal
Opt-in, so nothing changes by default. For example:
"tailwindcss/enforce-consistent-line-wrapping": ["error", {
"printWidth": 90,
"wrapLines": "overWidth",
"wrapStrings": "jsx" // default: "never"
}]- Applies only to a
Literalthat is the direct value of a JSX attribute (className="…"), never to a JS string (cn("…"),const classes = "…"). Those keep today's warn-only behaviour, since fixing them would need a string → template conversion. - Reuses the
rewrapTemplateToWidth/rewrapOverBudgetLinesToWidthlayout, and escapes nothing new. A JSX string can't contain its own quote, and class lists never do. classesPerLinecould follow the same switch.
Questions
- Is the "judgment call" in the docs about strings in general, or mainly about converting a JS string into a template? If it's the latter, JSX attributes seem to fall outside it.
- Option name and shape:
wrapStrings: "never" | "jsx", a boolean, or something else?
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
feat(enforce-consistent-line-wrapping): wrap a JSX attribute string i…
chore: release 1.15.0 (#226)
sergioazoc commentedon Oct 9, 2026
Out in 1.15.0 (#224) as
wrapStrings: "jsx".You were right about the "judgment call": it was about turning a JS string into a template, and a JSX attribute's own string doesn't need that, its newlines are just whitespace in a class attribute. So with
wrapStrings: "jsx"the rule wrapsclassName="…"in place, same block layout as templates, with the closing quote at the attribute line's indentation. BothwrapLinesandclassesPerLineapply, and JS strings (className={"…"},cn("…"), variables) keep reporting without a fix.Two details before you turn it on in that codebase with 328 of them. A CRLF file gets CRLF breaks (the value keeps the file's
\r\n, so it's measured and written back with the right ending). And if oxfmt formats withsortTailwindcss, setpreserveWhitespace: true, otherwise it collapses the block back to one line and the two keep undoing each other.Docs: https://oxlint-tailwindcss.pages.dev/rules/enforce-consistent-line-wrapping#wrapstrings
dl-web-dev commentedon Oct 9, 2026
Just curious, are there plans to also include
className={"…"},cn("…"), variables ? Want to fully migrate to this plugin but really like the way the readability of the wrapped strings fromeslint-plugin-better-tailwindcssin bothcnandclassName={"…"}sergioazoc commentedon Oct 10, 2026
Not yet, but it's a small step from
wrapStrings: "jsx", so I'll add it as an opt-in,wrapStrings: "all". It works like better-tailwindcss: when a JS string has to wrap, it becomes a template literal with the same block layout. That coverscn("…"),className={"…"}, ternary and&&branches, arrays,cva/tvvalues and variables. It never touches an object key (cn({ "…": cond })), or a string holding a backslash, a backtick or${.It stays opt-in because it changes the string's value. The newlines and indentation are just whitespace in a class list, but they matter if a variable matched by
variablePatternsis used for something else. I'll post here when it's out.feat(enforce-consistent-line-wrapping): wrapStrings: "all" turns JS s…
chore: release 1.16.0 (#234)
sergioazoc commentedon Oct 10, 2026
Out in 1.16.0 (#232) as
wrapStrings: "all". A JS string that has to wrap (cn("…"),className={"…"}, arrays,cva/tvvalues, ternary and&&branches, variables) becomes a template literal with the same block layout as your JSX strings, andclassName="…"is still wrapped in place.If oxfmt sorts your classes, set
preserveWhitespace: trueinsortTailwindcss. Otherwise it collapses the template back to one line and the two keep undoing each other, same as with the JSX strings.Docs: https://oxlint-tailwindcss.pages.dev/rules/enforce-consistent-line-wrapping#wrapstrings
dl-web-dev commentedon Oct 12, 2026
Much appreciated!