@shadcn/lint: the linter that enforces your AI agent to respect your design system in Tailwind CSS

@shadcn/lint is an open source linter by shadcn, for ESLint and Oxlint, that converts your Tailwind CSS design system into errors that your AI agent reads and fixes automatically. Each error explains what broke and what to use instead, based on your own components, variants, and theme. The agent writes p-[13px], runs npm run lint, gets the correct size, and fixes its own diff before a human reviewer sees it.

It’s MIT, published on npm as @shadcn/lint, and works on any project with Tailwind v4, whether or not you use shadcn/ui.

What is @shadcn/lint and what does a design system linter do?

A linter is a tool that analyzes your code without running it and reports what violates a set of rules. It’s the same thing ESLint or Biome do with JavaScript errors. @shadcn/lint applies that idea to your design system: it doesn’t check whether your Tailwind classes are valid, but how each component is allowed to be styled.

You declare what’s allowed: what classes a Button accepts, whether padding should stay within the spacing scale, whether loose colors like bg-pink-500 are forbidden. The plugin reports each violation with a correction taken from your own code.

shadcn describes it as an agent-first linter. The rules are normal lint rules, which run in your editor and in your CI like any other. What changes is the error message: it’s written so that a coding agent can act on it, not for a human to glance at.

Do you need shadcn/ui to use it?

No. The README says it works with your own components and your own Tailwind theme. Projects with shadcn/ui have automatic component and theme discovery through components.json. If you don’t use shadcn/ui, you specify where your components are with the ui or componentImports settings.

Why a linter and not rules in AGENTS.md?

Because a linter checks the outcome, and its error tells you what to do next. The README illustrates this with a Button that controls its own padding. You could enforce that in TypeScript by restricting the style prop, but the compiler error just says padding is not allowed. It doesn’t say how to size the button.

With @shadcn/lint, <Button className="p-4"> produces something more like a code review comment:

"p-4" is not allowed on <Button>: <Button> owns its spacing.
Use a size (sm, lg), or margin here or gap on the parent for space around it.
Add a size in components/ui/button.tsx only if the design explicitly calls for one.

Rules written in prose, in AGENTS.md or in a system prompt, depend on the agent remembering them and applying them. A lint command is a check the agent runs and can’t work around with arguments.

What rules does it include?

It comes with six rules:

Rule What it detects
no-restyle Changing a component’s style with className
no-raw-colors Loose colors like bg-pink-500
no-arbitrary-values Arbitrary values like p-[13px]
no-inline-styles Inline styles and <style> elements
no-unknown-classes Classes Tailwind can’t generate, like rounded-huge
require-static-classes Classes the linter can’t read, like `bg-${color}`

no-restyle does most of the work; the others close the loopholes.

How do you install it with ESLint or Oxlint?

The fastest way is to have your own agent install it. The README’s quickstart is a prompt:

Read https://github.com/shadcn-ui/lint/blob/a89d04792f340bcea26af2d539a9eec7285fcc77/SETUP.md
and set up @shadcn/lint in this project.

SETUP.md tells the agent to:

  • detect your package manager;
  • register the plugin in the linter you already use, or set up Oxlint if you don’t have one;
  • keep your existing rules.

It also explicitly tells it not to enable any rules. Deciding what’s allowed is up to you.

If you prefer to do it by hand, you need Node.js 20.19 or later.

ESLint (9.30 or later):

npm install -D @shadcn/lint eslint @typescript-eslint/parser

// eslint.config.mjs
import { plugin as shadcn } from "@shadcn/lint"
import tsParser from "@typescript-eslint/parser"
import { defineConfig } from "eslint/config"

export default defineConfig([
  {
    files: ["**/*.{js,jsx,ts,tsx}"],
    languageOptions: {
      parser: tsParser,
      parserOptions: { ecmaFeatures: { jsx: true } },
    },
    plugins: { shadcn },
    rules: {
      "shadcn/no-arbitrary-values": "error",
    },
  },
])

Oxlint (1.80 or later; its JS plugin API is still in alpha):

npm install -D @shadcn/lint oxlint

{
  "jsPlugins": ["@shadcn/lint"],
  "rules": {
    "shadcn/no-arbitrary-values": "error"
  }
}

Then add the command (eslint . or oxlint) as a lint script in package.json and put this line in AGENTS.md:

After making changes, run `npm run lint` and fix all errors.

That line closes the loop: without it, the agent doesn’t know the linter exists.

How do you write rules for your Tailwind CSS design system?

Start with three. Together they cover the most common drift: components restyled from outside, spacing out of scale, and hardcoded colors in code.

1. Let a Button move, but not change shape. A contract defines what classes a component accepts:

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  contracts: [
    { pattern: "^Button$", allow: ["w-full", "mt-*", "mb-*"] },
  ],
}]

<Button size="lg" className="mt-4 md:w-full" /> passes. <Button className="p-4 hover:rounded-full" /> fails, and <Button className="md:h-12 w-48" /> does too.

2. Allow spacing changes, but only within your scale. Combine a contract that opens spacing in CardContent with no-arbitrary-values:

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  contracts: [
    { pattern: "^CardContent$", allow: ["layout", "spacing"] },
  ],
}],
"shadcn/no-arbitrary-values": "error",

p-6 md:p-8 passes. md:p-[13px] fails.

3. Write the error in your own words. Custom messages accept placeholders, so the agent sees your component’s actual sizes:

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  message: {
    spacing: "Use a {{component}} size: {{sizes}}.",
  },
}]

For a Button with sizes sm and lg, the error reads Use a Button size: sm, lg. Turn on no-raw-colors alongside it and the agent will fetch your design tokens instead of writing bg-pink-500.

Contracts are tied by component name, so you can apply them to components from third-party packages without forking or wrapping them.

Does it actually work with AI agents?

According to shadcn’s own evaluations, yes. This is self-reported data, published in the repository with methodology and execution identifiers, but not independently reproduced.

Across more than 150 task runs, almost all reached zero violations after a single correction round:

  • On eight “temptation” tasks, asking for styles outside the system (a pink button, 13 px of padding, “make it stand out”), Sonnet 5 averaged about 70 violations per run before feedback. After that, it hit zero on 40 out of 40 tasks.
  • No model used an eslint-disable comment or an inline style to achieve it. The typical fix was adding a variant to the component, like variant="brand" instead of a hard-coded hex color.

The cost comparison is the most interesting part. In control runs, agents received the same rules as text and were asked to review their own work. Against that, fixing with linter diagnostics cost less across all three models:

  • Haiku 4.5: from $1.41 to $0.74
  • Sonnet 5: from $3.57 to $2.47
  • Opus 5: from $4.35 to $3.93

The smaller the model, the bigger the gain.

The evaluations page itself lists its limits: all runs used a single model family, and the judge scoring visual fidelity is from that same family.

@shadcn/lint or eslint-plugin-tailwindcss?

They solve different problems. eslint-plugin-tailwindcss enforces Tailwind hygiene: class order, abbreviations, contradictory classes, and custom class names. Its README notes that Tailwind v4 support is partial and only available in a beta channel. @shadcn/lint is built for Tailwind v4 and enforces your design system: which component accepts which class and what to use instead. If your problem is inconsistent class ordering, you need the first one. If your problem is an agent restyling your Button in every PR, you need the second.

Does it work with Vue and Svelte?

Yes, as of version 0.2.0 (released September 22, 2026), with a caveat. With ESLint it reads .vue and .svelte templates via vue-eslint-parser and svelte-eslint-parser. With Oxlint it only analyzes <script> blocks, because Oxlint can’t yet read those templates. At the time this note was published, the main repository README only covered React installation; the npm package and docs/ folder already cover all three frameworks.

What can’t it detect?

Anything outside your component markup. In shadcn’s red-team test, seven ways to bypass the rules were attempted, and two got through:

  • a CSS class written directly in globals.css;
  • a new theme token created for the occasion.

The first is a job for a CSS linter. The second is inside the system by definition, because the token lives in your theme. New tokens and variants still need human review: the lint passing doesn’t approve the design.

If you already give your agent a machine-readable design system, like Meta’s Astryx or a DESIGN.md file, @shadcn/lint is the half that makes it stick. Those tools tell the agent what the system looks like; this one checks if it listened.

Is it production-ready?

You can use it today, but it’s young. As of September 28, 2026, the package is on version 0.2.0, first released September 14. It’s had seven npm releases in eight days and no tagged releases on GitHub. Expect changes in option names and rule syntax before 1.0, and pin the version in package.json. What lasts is the idea: a design system expressed as checks the agent runs, not guidelines you ask it to remember.