English
max-class-count
oxlint rule that reports a Tailwind CSS class string with more classes than a set maximum (20 by default) — a sign it should be a component.
At a glance
| Autofix | Editor suggestions | Design system | Options |
|---|---|---|---|
| No | No | Not used | max |
What this rule does
Counts the Tailwind classes in each class string and reports when the count exceeds a configurable maximum. The diagnostic suggests extracting a component or utility — the assumption being that once an element needs more than ~20 classes, you're describing a component inline and the right move is to name it.
A class string is a string literal — className="…", one argument of cn()/clsx(), one cva()/tv() value — or a whole template literal, tw`…` included. In a template the parts around ${} are added together: a class glued to an expression (bg-${tone}-500) counts once, and a bare ${expr} counts as nothing, since its classes aren't known statically. Separate cn() arguments and variant values are counted one by one: they are often conditional or alternatives, and adding them up would report class lists that never apply together.
The counter is dumb on purpose: it counts whitespace-separated classes after the standard extractor has resolved the location. No deduplication, no semantic grouping, no DS lookup. That keeps it predictable: if your className reads as N classes, the rule sees N. Multiline strings rebuilt by enforce-consistent-line-wrapping are counted as the underlying class set, not by visual lines.
DS-independent — no entryPoint needed. No autofix: extracting a component requires judgement the rule can't make for you.
Options
max
number, default 20.
The maximum number of classes allowed in a single class string. The rule reports when classes.length > max (i.e. the limit is inclusive: max: 20 allows exactly 20). Adjust it to your team's threshold for "this is now a component."
jsonc
{ "tailwindcss/max-class-count": ["warn", { "max": 15 }] }Suggested ballparks:
max: 10-15— strict component-extraction culture, lots of small reusable building blocks.max: 20(default) — middle ground, fires on outliers without nagging on legitimately dense utility-first markup.max: 30+— large dashboard apps where dense one-off layouts are common and you only want a guard against truly egregious cases.
Examples
✗ Incorrect
tsx
// 21 classes — over the default max of 20
<div className="flex items-center p-4 m-2 bg-white text-black rounded shadow border w-full h-10 gap-2 justify-between font-bold text-sm overflow-hidden cursor-pointer transition duration-200 opacity-50 z-10" />
// reports: Too many Tailwind classes (21). Maximum allowed is 20.
// 6 classes with `max: 5`
// options: { "max": 5 }
<div className="flex items-center p-4 m-2 gap-2 w-full" />
// A template is one class string: 6 classes with `max: 5`
// options: { "max": 5 }
<div className={`flex items-center p-4 ${gap} m-2 bg-${tone}-500 w-full`} />✓ Correct
tsx
// Within the default limit
<div className="flex items-center p-4 m-2 gap-2" />
// Extracted into a component — count resets per element
function Card({ children }) {
return (
<div className="rounded-lg border bg-white p-4 shadow">
<div className="flex items-center gap-2">{children}</div>
</div>
)
}
// Each `cn()` argument is its own class string: 2 + 3, not 5, so `max: 4` is fine
// options: { "max": 4 }
cn("flex items-center", "p-4 m-2 gap-2")Interactions with other rules
no-duplicate-classes: if a class is repeated, both rules will see the bloated count. Fix the duplicate first — the count drops and this rule may stop firing on its own.enforce-sort-order/enforce-consistent-line-wrapping: cosmetic siblings. The counter is whitespace-insensitive so line-wrapping doesn't change the verdict.enforce-shorthand: merges pairs such asw-4 h-4→size-4. Once its fix is applied, a borderline class string can drop back under the limit.no-arbitrary-value: orthogonal, but related in spirit — both nudge toward extracting a component when a single element starts carrying too much markup-level logic.
When to disable it
- Files that are inherently dense (top-level layouts, marketing hero sections, design-system primitive components that are the abstraction). Prefer per-line disables or a higher
maxin the override block. - Generated markup (codegen, MDX components) where the count reflects the generator, not author intent.
- You disagree with the heuristic: the rule is opinionated and won't suit every codebase. There's no shame in turning it off — it exists for teams that want the nudge.