You paste a React component into an LLM and ask it to "change the padding on the header div." The LLM responds — but it modified the wrong <div>. You try again: "no, the second div, the one wrapping the title." Two more rounds of back and forth until it finally understands which element you meant.
I got tired of this. So I built eslint-plugin-jsx-classname — an ESLint plugin that enforces className on every HTML element in your JSX. Not for styling. For giving every element a name that both humans and LLMs can reference unambiguously.
The Problem: Anonymous Elements
In most React codebases, it's easy for bare HTML elements to slip through code review:
function Card({ title, children }) {
return (
<div>
<div>
<span>{title}</span>
</div>
<div>{children}</div>
</div>
);
}
Now tell an LLM to "add margin to the content wrapper." Which <div> is the content wrapper? The LLM has to guess based on structure, and it often guesses wrong. You end up spending more time correcting the AI than writing the code yourself.
Compare that to:
function Card({ title, children }) {
return (
<div className="card">
<div className="card-header">
<span className="card-title">{title}</span>
</div>
<div className="card-body">{children}</div>
</div>
);
}
Now "add margin to card-body" is unambiguous. The LLM knows exactly which element you mean. No back and forth. No guessing. One prompt, one correct edit.
Why This Matters Now
AI-assisted coding is becoming the norm. Whether you're using Claude, Copilot, or Cursor — you're constantly describing parts of your UI to an LLM. Semantic class names act as a shared vocabulary between you and the AI.
Without them, every conversation about your JSX becomes a game of "which div do I mean?" With them, you can point at any element by name. It's the difference between giving directions with street names versus "turn left at the third building after the tall one."
What the Plugin Does
eslint-plugin-jsx-classname provides a single rule: require-classname. It checks every HTML element in your JSX and reports when className is missing.
⚠ HTML element <div> is missing a className attribute.
It only checks standard HTML elements — custom components like <MyButton> or <Card> are ignored, since they already have a name.
Installation
npm install --save-dev eslint-plugin-jsx-classname
Configuration
The plugin uses ESLint flat config. The quickest way to get started:
// eslint.config.js
import jsxClassname from "eslint-plugin-jsx-classname";
export default [jsxClassname.configs.recommended];
This enables the rule as a warning with default options. For stricter enforcement:
export default [jsxClassname.configs.strict];
The strict config sets the rule to error level and enables ignoreTailwind (more on that below).
Customizing the Rule
Check Only Specific Elements
If enforcing className on every element is too aggressive for your codebase, narrow it down:
rules: {
"jsx-classname/require-classname": ["warn", {
elements: ["div", "span", "section"]
}]
}
Exclude Certain Elements
Some elements don't need names — <hr>, <br>, or <input type="hidden">:
rules: {
"jsx-classname/require-classname": ["warn", {
excludeElements: ["hr", "br", "input"]
}]
}
The Tailwind Trap
This is the option I'm most excited about. If your team uses Tailwind CSS, you've probably seen this:
<div className="flex items-center justify-between p-4 mt-2">
<span className="text-lg font-bold text-gray-800">{title}</span>
</div>
Technically, every element has a className. But these are all Tailwind utilities — the element still has no semantic identity. Tell an LLM "modify the flex items-center justify-between p-4 mt-2 div" and you're back to the same problem.
With ignoreTailwind: true, the rule requires at least one non-Tailwind class:
rules: {
"jsx-classname/require-classname": ["error", {
ignoreTailwind: true
}]
}
Now this fails:
<div className="flex items-center p-4" />
But this passes:
<div className="card-header flex items-center p-4" />
You get Tailwind utilities for styling, plus a semantic class name that gives the element a referenceable identity.
Under the hood, Tailwind detection uses tailwind-merge to programmatically identify utility classes. This means it stays current with Tailwind updates automatically, without maintaining a massive regex list.
"But That's a Lot of Work"
That's the first thing people say. Adding a meaningful className to every single HTML element? In the past, that would have been tedious busywork that no team would actually maintain.
But here's the thing — we're not writing all the code ourselves anymore. Your AI coding agent is already generating most of the JSX. With this lint rule in place, the agent simply adds semantic class names as it writes components. It's zero extra effort for you.
And to make sure the agent never skips the pre-commit hooks that enforce this rule, pair it with block-no-verify. This package prevents --no-verify from bypassing git hooks, so the lint rule is always enforced — no matter who or what is committing the code.
The combination is simple: the lint rule defines the standard, the agent does the work, and block-no-verify makes sure nothing slips through.
Give It a Try
npm install --save-dev eslint-plugin-jsx-classname
Check out the full documentation and source on GitHub. If it's useful to you, a star would mean a lot.
How do you handle communicating with LLMs about your UI components? I'd love to hear your approach in the comments.
Top comments (0)