# enforce-consistent-line-wrapping

oxlint rule that reports Tailwind CSS class strings over a print width or a classes-per-line budget,
and can wrap them onto several lines.

## At a glance

| Autofix | Editor suggestions | Design system                            | Options                                                                           |
| ------- | ------------------ | ---------------------------------------- | --------------------------------------------------------------------------------- |
| Yes     | No                 | Optional — used when `entryPoint` is set | `entryPoint`, `printWidth`, `classesPerLine`, `wrapLines`, `group`, `wrapStrings` |

## What this rule does

Flags long class strings so they don't sprawl past a sensible line length and, when you opt in,
wraps them into multiple lines. Two independent formatting modes: a width-based re-wrap (opt-in via
`wrapLines`) and a class-count budget per line (`classesPerLine`), both autofixing template literals
and, with `wrapStrings`, string literals too: a JSX attribute's own string in place (`"jsx"`), and a
JS string turned into a template (`"all"`). Otherwise string literals only report — a JS string
can't span lines.

DS-optional — works without `settings.tailwindcss.entryPoint`. The rule operates on the raw class
string and doesn't care what the classes mean, with one exception: when a design system **is**
available, the `wrapLines: "all"` grouping consults it for the Tailwind v4 project prefix
(`@import "tailwindcss" prefix(tw)`) so the prefix is transparent to grouping — `tw:flex` groups as
a base utility and `tw:hover:x` as `hover:`. Without an entry point the rule silently falls back to
treating the prefix as part of the variant chain (it never reports a missing design system).

Both fixers wrap template literals into the **block convention**: the content starts on its own
line, each wrapped line is indented one level below the statement's own indentation, and the closing
backtick (or quote, for a JSX attribute string) sits on its own line. The base indentation is read
from the source line (not the backtick column), so the block nests correctly even inside
deeply-indented JSX. The `classesPerLine` fix and the `"overWidth"` width fix are
**non-destructive** — a template that is already wrapped only has its over-budget lines re-wrapped;
conforming lines are left untouched. The `"all"` width fix is a **full re-layout**: it lays the
whole template out in the canonical variant-grouped form, replacing any hand layout (which is why
it's a separate, explicit mode). Other rules (`no-unnecessary-whitespace`, `enforce-sort-order`, …)
are aware of this multiline shape and won't collapse it back.

## Options

### `printWidth`

`number`, default `80`.

Maximum length of any **single line** of the class string. For a one-line string that is its whole
length; for a multiline template it is the longest individual line, so splitting a long string
across lines can actually satisfy the rule. A line whose single class alone exceeds the width is not
reported — it can't be wrapped any shorter.

On its own, `printWidth` is **warn-only**: it reports `tooLong` and never rewrites your code. The
autofix is opt-in via `wrapLines`.

```jsonc
{ "tailwindcss/enforce-consistent-line-wrapping": ["error", { "printWidth": 100 }] }
```

### `wrapLines`

`"overWidth" | "all"`, optional. No default — like `classesPerLine`, leaving it unset means the
fixer is off and `printWidth` only reports.

Turns on the **width-based autofix** for template literals. The two modes differ in both reach and
layout:

- `"overWidth"`: only the **lines** that actually exceed `printWidth` are re-wrapped — each is
  greedily packed into the fewest lines that fit the budget (no variant grouping), reusing the
  line's own indentation. Everything else is left verbatim: templates whose lines all fit, and the
  conforming lines of a template that does get touched, keep their hand-formatted layout. A
  single-line template has no layout to preserve, so it converts to the block convention.
- `"all"`: **every** multiline (or over-width) template is normalized to a canonical layout,
  mirroring `eslint-plugin-better-tailwindcss`'s fixer: classes are grouped into runs sharing a
  variant chain (`hover:`, `md:hover:`, or none), and within a run classes are packed onto a line
  until adding the next would exceed `printWidth` (indentation included). How the runs are separated
  is controlled by `group` (below). Within-budget templates that don't match that layout report
  `inconsistentWrapping`.

In a JS string literal (`cn("…")`, `className={"…"}`, a variable) the rule reports `tooLong` without
a fix by default: a JS string can't hold a raw newline, and turning it into a multiline template
changes the string's value, so that is opt-in — see `wrapStrings`, which can also wrap a JSX
attribute's own string as it is. A template fragment **glued** to a `${}` with no whitespace
(`` `${a}flex …` `` — one runtime class) is never autofixed: any whitespace introduced at the
boundary would split that class in two.

Around an interpolation, the `"all"` layout puts the class run bordering a `${}` on its **own fresh
line** (never hanging inline after the expression), so every rewritten line stays within
`printWidth`. One caveat: the width of the `${…}` expression itself is opaque to the rule, which
measures each static fragment on its own. So a single physical line you hand-write with a short
fragment beside a wide interpolation can still exceed `printWidth` without being reported.

`wrapLines` only applies when `classesPerLine` is **not** set — `classesPerLine` owns the layout
otherwise.

```jsonc
{
  "tailwindcss/enforce-consistent-line-wrapping": [
    "error",
    { "printWidth": 100, "wrapLines": "overWidth" },
  ],
}
```

### `group`

`"newLine" | "emptyLine" | "never"`, default `"newLine"`.

How the `wrapLines: "all"` layout separates variant groups. Matches
`eslint-plugin-better-tailwindcss`'s `group` option (same name, values, and default), so a migrated
config carries over unchanged.

- `"newLine"` (default): each variant run starts its own line.
- `"emptyLine"`: additionally, a **blank line** separates the runs (the blank lines carry no
  trailing whitespace, and `no-unnecessary-whitespace` preserves them).
- `"never"`: no grouping — classes are packed greedily across run boundaries into the fewest lines
  that fit `printWidth`.

`group` only shapes the `"all"` layout: `"overWidth"` deliberately never re-groups (it only splits
over-budget lines), and the `classesPerLine` fixer chunks by count, so both ignore it.

```jsonc
{
  "tailwindcss/enforce-consistent-line-wrapping": [
    "error",
    { "printWidth": 100, "wrapLines": "all", "group": "emptyLine" },
  ],
}
```

### `classesPerLine`

`number`, optional. No default.

Maximum number of classes on a single line. When exceeded inside a template literal (`` `…` ``), the
rule autofixes by wrapping the classes into the block convention, chunks of `classesPerLine` per
line. Inside a string literal the rule reports `tooManyPerLine` without a fix, unless `wrapStrings`
covers it.

Setting `classesPerLine` switches the template-literal fixer to this chunk-based mode and turns the
width-based (variant-grouped) fixer off — `printWidth` then only reports, and `wrapLines` is
ignored.

```jsonc
{ "tailwindcss/enforce-consistent-line-wrapping": ["error", { "classesPerLine": 5 }] }
```

### `wrapStrings`

`"never" | "jsx" | "all"`, default `"never"`.

Which string literals the fixers may wrap, besides template literals. Both `wrapLines` and
`classesPerLine` apply, and a file with CRLF line endings gets CRLF breaks.

With `"jsx"`, a JSX attribute's own string — `className="…"`, or any attribute the plugin reads — is
wrapped in place into the block convention, the closing quote at the attribute line's indentation:

```tsx
<div
  className="
    flex items-center justify-between rounded-md px-3 py-1
    text-sm shadow-md
  "
/>
```

That is valid JSX as written: in a class attribute the newlines are plain whitespace, so nothing is
converted — it's the layout `eslint-plugin-better-tailwindcss` writes. A JS string —
`className={"…"}`, `cn("…")`, a variable — can't hold a raw newline, so with `"jsx"` it keeps
reporting without a fix.

With `"all"`, the JSX string is wrapped the same way, and a JS string is turned into a template
literal with the block layout, as `eslint-plugin-better-tailwindcss` does: in a call (`cn("…")`), in
`className={"…"}`, in an array, as an object value (`cva`, `tv`), in a ternary or `&&` branch, and
as a variable's value.

```tsx
const card = cn(`
  flex items-center justify-between rounded-md px-3 py-1
  text-sm shadow-md
`)
```

An object key (`cn({ "…": isActive })`) and a `+` operand are never turned into a template, and
neither is a string holding an escape sequence, a backtick or `${`, which a template reads
differently: those keep reporting without a fix. The string's value gains the newlines and the
indentation. In a class list that is whitespace, but if your code uses a class string as something
else — splits or compares a variable that `variablePatterns` matches — stay on `"jsx"`.

If oxfmt formats your files with `sortTailwindcss`, set its `preserveWhitespace: true`: by default
it collapses the whitespace in class strings, a JSX attribute's and a helper's template alike, the
block's line breaks included, and the two undo each other (see [/interop](/interop)).

```jsonc
{
  "tailwindcss/enforce-consistent-line-wrapping": [
    "error",
    { "printWidth": 100, "wrapLines": "overWidth", "wrapStrings": "all" },
  ],
}
```

### `entryPoint`

`string`, optional.

Per-rule override of `settings.tailwindcss.entryPoint`. The rule consults the design system only for
the Tailwind v4 project prefix, and only under `wrapLines: "all"` with `group` `"newLine"` or
`"emptyLine"` (see above); everywhere else it has no effect. Almost never needed.

## Examples

### ✗ Incorrect

```tsx
// Exceeds default printWidth of 80 characters — reports; without `wrapLines` there is no autofix
<div className="flex items-center justify-between p-4 m-2 bg-white text-black rounded shadow-lg border w-full" />
//              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ tooLong

// 6 classes with classesPerLine: 3 — template literal autofixes into a block
// options: { "classesPerLine": 3 }
const className = `flex items-center justify-between p-4 m-2 bg-white`
// → const className = `
//     flex items-center justify-between
//     p-4 m-2 bg-white
//   `

// Same count in a JSX string — reports; without wrapStrings: "jsx" there is no autofix
// options: { "classesPerLine": 3 }
<div className="flex items-center justify-between p-4 m-2 bg-white" />

// The same JSX string with wrapStrings: "jsx" — wrapped in place, between the quotes
// options: { "classesPerLine": 3, "wrapStrings": "jsx" }
<div className="flex items-center justify-between p-4 m-2 bg-white" />
// → <div className="
//     flex items-center justify-between
//     p-4 m-2 bg-white
//   " />

// A JS string with wrapStrings: "all" — turned into a template literal
// options: { "classesPerLine": 3, "wrapStrings": "all" }
const card = cn("flex items-center justify-between p-4 m-2 bg-white")
// → const card = cn(`
//     flex items-center justify-between
//     p-4 m-2 bg-white
//   `)

// printWidth: 40 with wrapLines: "overWidth" — ONLY the over-budget
// line is re-wrapped; the conforming lines around it stay exactly as written
// options: { "printWidth": 40, "wrapLines": "overWidth" }
const className = `
  flex hover:underline
  items-center justify-between gap-4 rounded-lg p-6
  focus:outline-none
`
// → const className = `
//     flex hover:underline
//     items-center justify-between gap-4
//     rounded-lg p-6
//     focus:outline-none
//   `

// printWidth: 40 with wrapLines: "all" — full re-layout, grouped by variant
// options: { "printWidth": 40, "wrapLines": "all" }
const className = `flex items-center gap-2 hover:bg-red-500 hover:underline focus:outline-none`
// → const className = `
//     flex items-center gap-2
//     hover:bg-red-500 hover:underline
//     focus:outline-none
//   `
```

### ✓ Correct

```tsx
// Fits within printWidth
<div className="flex items-center p-4" />

// Already wrapped at classesPerLine (block convention)
const className = `
  flex items-center p-4
  bg-white text-black
`

// Hand-formatted multiline template, every line within printWidth — untouched
// when wrapLines is unset or "overWidth"; only "all" re-groups it
const className = `
  flex hover:underline
  items-center
`

// A JSX string already in the block convention
// options: { "classesPerLine": 3, "wrapStrings": "jsx" }
<div className="
  flex items-center p-4
  bg-white text-black
" />
```

## Interactions with other rules

- **`no-unnecessary-whitespace`**: deliberately preserves the `\n` + indent introduced by this rule.
  The two were designed to coexist; without that preservation, fixers would oscillate (issue #14).
- **`enforce-sort-order`**: rebuilds class strings from a token list via `rebuildClassString`, which
  keeps the multiline separators. The sort fixer and the wrap fixer run on the same string without
  fighting. The width fixer never reorders classes — it only chooses where the line breaks go — so
  sorting stays entirely `enforce-sort-order`'s job.
- **All extractor-driven rules**: `splitClassesWithSeparators` is multiline-aware, so every other
  rule (`enforce-canonical`, `enforce-shorthand`, …) reports against multiline class strings the
  same way it does against single-line ones.

## When to disable it

- **You let `prettier` handle JSX line wrapping**: prettier breaks at the JSX-attribute level, not
  inside the string. The two are complementary, but if you don't want any in-string wrapping at all,
  disable.
- **Working with code generators** that emit very long class strings intentionally (e.g. CMS-driven
  content classes).
- **You use a different convention** like "always one class per line": this rule's chunk-based
  wrapping doesn't model that.
