Vibe Coding Forem

Cover image for Why I Built an ESLint Rule That Forces className on Every HTML Element
tupe12334
tupe12334

Posted on

Why I Built an ESLint Rule That Forces className on Every HTML Element

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

This enables the rule as a warning with default options. For stricter enforcement:

export default [jsxClassname.configs.strict];
Enter fullscreen mode Exit fullscreen mode

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"]
  }]
}
Enter fullscreen mode Exit fullscreen mode

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"]
  }]
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
  }]
}
Enter fullscreen mode Exit fullscreen mode

Now this fails:

<div className="flex items-center p-4" />
Enter fullscreen mode Exit fullscreen mode

But this passes:

<div className="card-header flex items-center p-4" />
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)