shadcn-ui/lint

An agent-first linter for Tailwind design systems. Write design system rules that agents can verify.

TypeScript

3,178

26 commits

updated Oct 10, 2026

See the code

README

@shadcn/lint

Write design system rules that agents can verify.

A lint diagnostic explains why padding is not allowed on Button and suggests using an existing size.

@shadcn/lint is an agent-first linter for Tailwind design systems.

You define what’s allowed. When an agent breaks a rule, the error explains what’s wrong and suggests a fix based on your components, variants, and theme.

Works with your existing design system. No rewrite required.

@shadcn/lint works with Tailwind v4 projects (shadcn/ui not required). Available for both ESLint and Oxlint. Works with React, Svelte, and Vue.

Table of contents

Quickstart

Give your coding agent this prompt:

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

Once installed, choose your rules and configure what’s allowed in your design system.

Prefer to configure it yourself? See Get started.

TypeScript vs @shadcn/lint

Take a Button that allows margin and width, but controls its own padding. You can enforce that with types by limiting its style prop to Pick<React.CSSProperties, "margin" | "width">.

<Button style={{ padding: 16 }}>Submit</Button>
TS2353: Object literal may only specify known properties, and 'padding' does not exist in type 'Pick<CSSProperties, "margin" | "width">'.

The rule works. But this error only tells the agent that padding is not allowed. It doesn’t tell it how to size the Button.

With @shadcn/lint, the same rule comes with guidance from your design system:

<Button className="p-4">Submit</Button>
"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.

You decide what can change

Expressing these policies in TypeScript can take complex types. With @shadcn/lint, you configure them without changing your component API.

Here are some examples.

Allow spacing with margin. Allow full width. Keep size and shape in the Button.

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  contracts: [
    { pattern: "^Button$", allow: ["w-full", "mt-*", "mb-*"] },
  ],
}]
// Allowed: use a size and let the page control placement and full width.
<Button size="lg" className="mt-4 md:w-full" />

// Error: you are not allowed to change padding and shape.
<Button className="p-4 hover:rounded-full" />

// Error: you are not allowed to set a custom height or fixed width.
<Button className="md:h-12 w-48" />

Give each part of a component its own rules.

Let Card titles change typography, but keep their font family and weight. Let Card content change spacing, but keep its typography.

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  contracts: [
    {
      pattern: "^CardTitle$",
      allow: ["layout", "typography"],
      deny: ["font-*"],
    },
    { pattern: "^CardContent$", allow: ["layout", "spacing"] },
  ],
}]
// Allowed: titles can change text size; content can change padding.
<CardTitle className="text-lg" />
<CardContent className="p-6" />

// Error: you are not allowed to change the title’s font weight.
<CardTitle className="md:font-bold" />

// Error: you are not allowed to change the content’s typography.
<CardContent className="text-lg" />

Allow spacing changes. Require theme values.

Opening up spacing doesn’t have to mean allowing arbitrary values. Combine rules to let Card content change padding while keeping it on your theme’s spacing scale.

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  contracts: [
    { pattern: "^CardContent$", allow: ["layout", "spacing"] },
  ],
}],
"shadcn/no-arbitrary-values": "error",
// Allowed: padding uses the theme’s spacing scale.
<CardContent className="p-6 md:p-8" />

// Error: you are not allowed to use an arbitrary padding value.
<CardContent className="md:p-[13px]" />

Both approaches enforce the rule. With @shadcn/lint, the agent also sees how to fix the code using what’s already in your design system.

Built for agents

We built @shadcn/lint for agents that write UI. The errors tell them what broke, what to use instead, and where to find it. Suggestions come from your components, variants, and theme.

You can add custom messages and contracts so agents get your design system’s instructions with the error.

It works

We tested these rules with coding agents across more than 150 task runs. Almost every task reached zero violations in one correction round.

Here are the errors before and after lint feedback in one run per model:

ModelCompleted tasksErrors beforeErrors after
Sonnet 58/8690
Haiku 4.58/8660
Opus 58/8420
GPT 5.6 Terra8/81170
GPT 5.6 Sol6/8980

It is cheaper

In the Claude control runs, fixing violations with lint feedback cost 10% to 48% less than with rules alone.

See the evals for results and methodology.

Why a linter?

A linter is programmable. You can write rules for your design system without changing your components.

You define what’s allowed and what to use instead. Agents run your lint command to check their work.

  • Ship the same components with different rules. Each project can define its own contracts without changing the component code.
  • Use components you don’t own. Apply rules to components from third-party packages. No forks. No wrappers.
  • Share rules across projects. Keep a shared configuration for your design system and let projects add their own rules.

Your components stay flexible. You decide how they should be used.

Programmable

Custom messages

You can write custom error messages that tell agents what to do.

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  message: {
    spacing: "Use the size prop instead of padding.",
  },
}]

When an agent writes:

<Button className="p-4">Save changes</Button>

It sees:

Use the size prop instead of padding.

Placeholders

Use your component’s sizes, variants, and file paths in error messages. For spacing errors, {{sizes}} lists the available sizes:

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

For a Button with sm and lg sizes, the error becomes:

Use a Button size: sm, lg.

You can also tell agents where to find theme colors. For example, in no-raw-colors:

message: "Use a theme color from {{file}}."

If your theme is in src/index.css, the error becomes:

Use a theme color from src/index.css.

See all message placeholders.

Contracts

Give each component its own rules. For example, let pages change a card title's typography:

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  contracts: [
    { pattern: "^CardTitle$", allow: ["layout", "typography"] },
  ],
}]
// Allowed by the contract.
<CardTitle className="text-sm">Account settings</CardTitle>

// Reported: the contract does not allow color overrides.
<CardTitle className="text-pink-500">Account settings</CardTitle>

See contracts and custom messages.

Get started

Requires Node.js 20.19 or later and a version supported by your linter.

React

ESLint

Requires ESLint 9.30 or later.

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

Create eslint.config.mjs. If your framework already configures ESLint, keep its parser setup and add the plugin, rule, and component override.

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",
    },
  },
])
npx eslint .

Oxlint

Requires Oxlint 1.80 or later.

npm install -D @shadcn/lint oxlint

Create .oxlintrc.json:

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

Vue

ESLint

Requires ESLint 9.30 or later.

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

Create eslint.config.mjs. If you already use eslint-plugin-vue, keep its parser setup and add the plugin and rule to that block.

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

export default defineConfig([
  {
    files: ["**/*.vue"],
    languageOptions: {
      parser: vueParser,
      parserOptions: { parser: tsParser },
    },
    plugins: { shadcn },
    rules: {
      "shadcn/no-arbitrary-values": "error",
    },
  },
])
npx eslint .

Oxlint

Requires Oxlint 1.80 or later. Oxlint reads the <script> blocks of .vue files, not the template. See the Oxlint limitation.

npm install -D @shadcn/lint oxlint

Create .oxlintrc.json:

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

Svelte

ESLint

Requires ESLint 9.30 or later.

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

Create eslint.config.mjs. If you already use eslint-plugin-svelte, keep its parser setup and add the plugin and rule to that block.

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

export default defineConfig([
  {
    files: ["**/*.svelte"],
    languageOptions: {
      parser: svelteParser,
      parserOptions: { parser: tsParser },
    },
    plugins: { shadcn },
    rules: {
      "shadcn/no-arbitrary-values": "error",
    },
  },
])
npx eslint .

Oxlint

Requires Oxlint 1.80 or later. Oxlint reads the <script> blocks of .svelte files, not the markup. See the Oxlint limitation.

npm install -D @shadcn/lint oxlint

Create .oxlintrc.json:

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

After setup

Add your chosen command (eslint . or oxlint) as the lint script in package.json. Then put this in AGENTS.md:

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

Each framework has its own page with the full setup and what is read: React, Vue, Svelte.

Rules

We developed these rules by studying production design systems and testing them with coding agents. They’re built for Tailwind, with errors that help agents follow your design system.

RuleWhat it catches
no-restyleRestyling a component with className.
no-raw-colorsRaw colors such as bg-pink-500.
no-arbitrary-valuesArbitrary values such as p-[13px].
no-inline-stylesInline styles and <style> elements.
no-unknown-classesClasses Tailwind cannot generate, such as rounded-huge.
require-static-classesComponent classes the linter cannot read, such as `bg-${color}`.

See rule options and how to add more rules.

Frameworks

The same rules, options, contracts, and messages run on React, Vue, and Svelte. See also Vue and Svelte docs.

FeatureReact (JSX)VueSvelte
Static class stringclassName="p-4"class="p-4"class="p-4"
Expression classclassName={...}:class="..."class={...}
Text and expression mixedtemplate literalclass + :class on one elementclass="p-4 {expr}"
Arrays and objects (clsx shape)yes:class="[...]", :class="{...}"class={[...]}, class={{...}}
Helper calls (cn, cva, tv, ...)yesyes, in script and templateyes, in script and template
One-hop variable resolutionyesyes, template to <script setup>yes
Spread with a class{...{ className }}v-bind="{ class }"{...{ class }}
Class directiven/an/aclass:name={cond}
Style as CSS textno, JSX has no string stylestyle="color: red"style="color: red"
Style objectstyle={{ ... }}:style="{ ... }"n/a
Style directiven/an/astyle:prop={value}
<style> element or blockreportednot readnot read
SVG color attributesfill, stroke, ...samesame
Component identityexport namefile name, <CardTitle> or <card-title>file name, <Card.Title> or <Title>
Contractsby component namesame names as Reactsame names as React
Variants from cva / tvcomponent filecomponent file, or the barrel beside itcomponent file, incl. <script module>
Variants from typed propsyesnono
Received class prop acceptedclassName paramprops.class from defineProps()class from $props()
Wrappers across filesclassName forwardingprops.class, v-bind="$attrs", single-root fallthroughclassName and {...rest} forwarding
Base UI render propyesn/an/a
Dynamic element<Comp> from a variable<component :is><svelte:element>
Suggestions rewrite sourceyesyesyes
require-static-classesyesyesyes
no-unknown-classesyesyesyes
Project without components.jsonyes, componentImportsyes, componentImportsyes, componentImports
ESLintyesyes, vue-eslint-parseryes, svelte-eslint-parser
Oxlintyesscript blocks only, warns oncescript blocks only, warns once

Dynamic elements get the token rules but not no-restyle. <style> blocks in .vue and .svelte files are plain CSS; use a CSS linter for them.

Settings

Use settings.shadcn to configure component imports, class functions, and guidance shared across rules.

You don’t need shadcn/ui to use @shadcn/lint. It works with your own Tailwind components and theme.

shadcn/ui projects get automatic component and theme discovery via components.json.

For a custom setup, add settings at the root of .oxlintrc.json. Include only the settings you need:

{
  "settings": {
    "shadcn": {
      "ui": "@/ds",
      "componentImports": ["^@acme/ui(/|$)"],
      "ignoreImports": ["^@acme/ui/internal(/|$)"],
      "mergeFunctions": ["customMerge"],
      "variantFunctions": ["variants"],
      "note": "See DESIGN.md for design rules and approved exceptions."
    }
  }
}

For ESLint, add the same settings object to the config object containing your rules.

SettingWhat it does
uiRecognizes component imports by prefix. @/ds matches @/ds and @/ds/button, but not @/dsx.
componentImportsRecognizes component imports using regex patterns. Use it for additional directories or packages.
ignoreImportsSkips component recognition for imports matching these regex patterns. Takes precedence over recognition.
mergeFunctionsAdds functions whose arguments contain classes, such as customMerge("mt-4", "w-full").
variantFunctionsAdds functions whose object values contain classes.
noteAppends your text to every rule's error or warning.

All settings except note accept a string or an array of strings. note accepts a string.

The built-in class functions are cn, cx, clsx, cva, tv, twMerge, twJoin, and classNames. The built-in variant functions are cva and tv. Your function lists add to these defaults.

A recognition option set on a rule overrides its shared setting. ui prefixes always apply alongside componentImports. Recognition settings do not apply to no-inline-styles; note applies to every rule.

When you change the component directory, update the setup's directory override too, for example src/ds/**. See rule options for more examples.

Monorepos

Use your workspace package's import prefix for shared components. For a UI package in packages/ui, add this to the root .oxlintrc.json:

{
  "jsPlugins": ["@shadcn/lint"],
  "settings": {
    "shadcn": {
      "ui": "@workspace/ui/components"
    }
  },
  "rules": {
    "shadcn/no-restyle": ["error", { "allow": ["layout"] }]
  },
  "overrides": [
    {
      "files": ["packages/ui/src/components/**"],
      "rules": { "shadcn/no-restyle": "off" }
    }
  ]
}

Apps can use the shared components:

import { Button } from "@workspace/ui/components/button"

export function SaveButton() {
  return <Button className="w-full">Save changes</Button>
}

For ESLint, use the same settings and rules in the setup above, and change the component-directory override to packages/ui/src/components/**. These paths assume your lint config is at the workspace root.

The linter resolves components through your apps' TypeScript paths and package exports. If each app's components.json already points to the shared UI package, you can omit settings.shadcn.ui. Each app keeps its own theme configuration.

Documentation

See the documentation for rule examples, configuration, troubleshooting, and evals.

Contributing

Please read the contributing guide.

License

Licensed under the MIT license.

agents
ai
design
design-system
design-tools
shadcn
shadcn-ui
tailwindcss

shadcn-ui/lint

An agent-first linter for Tailwind design systems. Write design system rules that agents can verify.

TypeScript

3,178

26 commits

updated Oct 10, 2026

See the code

README

@shadcn/lint

Write design system rules that agents can verify.

A lint diagnostic explains why padding is not allowed on Button and suggests using an existing size.

@shadcn/lint is an agent-first linter for Tailwind design systems.

You define what’s allowed. When an agent breaks a rule, the error explains what’s wrong and suggests a fix based on your components, variants, and theme.

Works with your existing design system. No rewrite required.

@shadcn/lint works with Tailwind v4 projects (shadcn/ui not required). Available for both ESLint and Oxlint. Works with React, Svelte, and Vue.

Table of contents

Quickstart

Give your coding agent this prompt:

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

Once installed, choose your rules and configure what’s allowed in your design system.

Prefer to configure it yourself? See Get started.

TypeScript vs @shadcn/lint

Take a Button that allows margin and width, but controls its own padding. You can enforce that with types by limiting its style prop to Pick<React.CSSProperties, "margin" | "width">.

<Button style={{ padding: 16 }}>Submit</Button>
TS2353: Object literal may only specify known properties, and 'padding' does not exist in type 'Pick<CSSProperties, "margin" | "width">'.

The rule works. But this error only tells the agent that padding is not allowed. It doesn’t tell it how to size the Button.

With @shadcn/lint, the same rule comes with guidance from your design system:

<Button className="p-4">Submit</Button>
"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.

You decide what can change

Expressing these policies in TypeScript can take complex types. With @shadcn/lint, you configure them without changing your component API.

Here are some examples.

Allow spacing with margin. Allow full width. Keep size and shape in the Button.

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  contracts: [
    { pattern: "^Button$", allow: ["w-full", "mt-*", "mb-*"] },
  ],
}]
// Allowed: use a size and let the page control placement and full width.
<Button size="lg" className="mt-4 md:w-full" />

// Error: you are not allowed to change padding and shape.
<Button className="p-4 hover:rounded-full" />

// Error: you are not allowed to set a custom height or fixed width.
<Button className="md:h-12 w-48" />

Give each part of a component its own rules.

Let Card titles change typography, but keep their font family and weight. Let Card content change spacing, but keep its typography.

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  contracts: [
    {
      pattern: "^CardTitle$",
      allow: ["layout", "typography"],
      deny: ["font-*"],
    },
    { pattern: "^CardContent$", allow: ["layout", "spacing"] },
  ],
}]
// Allowed: titles can change text size; content can change padding.
<CardTitle className="text-lg" />
<CardContent className="p-6" />

// Error: you are not allowed to change the title’s font weight.
<CardTitle className="md:font-bold" />

// Error: you are not allowed to change the content’s typography.
<CardContent className="text-lg" />

Allow spacing changes. Require theme values.

Opening up spacing doesn’t have to mean allowing arbitrary values. Combine rules to let Card content change padding while keeping it on your theme’s spacing scale.

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  contracts: [
    { pattern: "^CardContent$", allow: ["layout", "spacing"] },
  ],
}],
"shadcn/no-arbitrary-values": "error",
// Allowed: padding uses the theme’s spacing scale.
<CardContent className="p-6 md:p-8" />

// Error: you are not allowed to use an arbitrary padding value.
<CardContent className="md:p-[13px]" />

Both approaches enforce the rule. With @shadcn/lint, the agent also sees how to fix the code using what’s already in your design system.

Built for agents

We built @shadcn/lint for agents that write UI. The errors tell them what broke, what to use instead, and where to find it. Suggestions come from your components, variants, and theme.

You can add custom messages and contracts so agents get your design system’s instructions with the error.

It works

We tested these rules with coding agents across more than 150 task runs. Almost every task reached zero violations in one correction round.

Here are the errors before and after lint feedback in one run per model:

ModelCompleted tasksErrors beforeErrors after
Sonnet 58/8690
Haiku 4.58/8660
Opus 58/8420
GPT 5.6 Terra8/81170
GPT 5.6 Sol6/8980

It is cheaper

In the Claude control runs, fixing violations with lint feedback cost 10% to 48% less than with rules alone.

See the evals for results and methodology.

Why a linter?

A linter is programmable. You can write rules for your design system without changing your components.

You define what’s allowed and what to use instead. Agents run your lint command to check their work.

  • Ship the same components with different rules. Each project can define its own contracts without changing the component code.
  • Use components you don’t own. Apply rules to components from third-party packages. No forks. No wrappers.
  • Share rules across projects. Keep a shared configuration for your design system and let projects add their own rules.

Your components stay flexible. You decide how they should be used.

Programmable

Custom messages

You can write custom error messages that tell agents what to do.

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  message: {
    spacing: "Use the size prop instead of padding.",
  },
}]

When an agent writes:

<Button className="p-4">Save changes</Button>

It sees:

Use the size prop instead of padding.

Placeholders

Use your component’s sizes, variants, and file paths in error messages. For spacing errors, {{sizes}} lists the available sizes:

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

For a Button with sm and lg sizes, the error becomes:

Use a Button size: sm, lg.

You can also tell agents where to find theme colors. For example, in no-raw-colors:

message: "Use a theme color from {{file}}."

If your theme is in src/index.css, the error becomes:

Use a theme color from src/index.css.

See all message placeholders.

Contracts

Give each component its own rules. For example, let pages change a card title's typography:

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  contracts: [
    { pattern: "^CardTitle$", allow: ["layout", "typography"] },
  ],
}]
// Allowed by the contract.
<CardTitle className="text-sm">Account settings</CardTitle>

// Reported: the contract does not allow color overrides.
<CardTitle className="text-pink-500">Account settings</CardTitle>

See contracts and custom messages.

Get started

Requires Node.js 20.19 or later and a version supported by your linter.

React

ESLint

Requires ESLint 9.30 or later.

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

Create eslint.config.mjs. If your framework already configures ESLint, keep its parser setup and add the plugin, rule, and component override.

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",
    },
  },
])
npx eslint .

Oxlint

Requires Oxlint 1.80 or later.

npm install -D @shadcn/lint oxlint

Create .oxlintrc.json:

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

Vue

ESLint

Requires ESLint 9.30 or later.

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

Create eslint.config.mjs. If you already use eslint-plugin-vue, keep its parser setup and add the plugin and rule to that block.

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

export default defineConfig([
  {
    files: ["**/*.vue"],
    languageOptions: {
      parser: vueParser,
      parserOptions: { parser: tsParser },
    },
    plugins: { shadcn },
    rules: {
      "shadcn/no-arbitrary-values": "error",
    },
  },
])
npx eslint .

Oxlint

Requires Oxlint 1.80 or later. Oxlint reads the <script> blocks of .vue files, not the template. See the Oxlint limitation.

npm install -D @shadcn/lint oxlint

Create .oxlintrc.json:

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

Svelte

ESLint

Requires ESLint 9.30 or later.

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

Create eslint.config.mjs. If you already use eslint-plugin-svelte, keep its parser setup and add the plugin and rule to that block.

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

export default defineConfig([
  {
    files: ["**/*.svelte"],
    languageOptions: {
      parser: svelteParser,
      parserOptions: { parser: tsParser },
    },
    plugins: { shadcn },
    rules: {
      "shadcn/no-arbitrary-values": "error",
    },
  },
])
npx eslint .

Oxlint

Requires Oxlint 1.80 or later. Oxlint reads the <script> blocks of .svelte files, not the markup. See the Oxlint limitation.

npm install -D @shadcn/lint oxlint

Create .oxlintrc.json:

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

After setup

Add your chosen command (eslint . or oxlint) as the lint script in package.json. Then put this in AGENTS.md:

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

Each framework has its own page with the full setup and what is read: React, Vue, Svelte.

Rules

We developed these rules by studying production design systems and testing them with coding agents. They’re built for Tailwind, with errors that help agents follow your design system.

RuleWhat it catches
no-restyleRestyling a component with className.
no-raw-colorsRaw colors such as bg-pink-500.
no-arbitrary-valuesArbitrary values such as p-[13px].
no-inline-stylesInline styles and <style> elements.
no-unknown-classesClasses Tailwind cannot generate, such as rounded-huge.
require-static-classesComponent classes the linter cannot read, such as `bg-${color}`.

See rule options and how to add more rules.

Frameworks

The same rules, options, contracts, and messages run on React, Vue, and Svelte. See also Vue and Svelte docs.

FeatureReact (JSX)VueSvelte
Static class stringclassName="p-4"class="p-4"class="p-4"
Expression classclassName={...}:class="..."class={...}
Text and expression mixedtemplate literalclass + :class on one elementclass="p-4 {expr}"
Arrays and objects (clsx shape)yes:class="[...]", :class="{...}"class={[...]}, class={{...}}
Helper calls (cn, cva, tv, ...)yesyes, in script and templateyes, in script and template
One-hop variable resolutionyesyes, template to <script setup>yes
Spread with a class{...{ className }}v-bind="{ class }"{...{ class }}
Class directiven/an/aclass:name={cond}
Style as CSS textno, JSX has no string stylestyle="color: red"style="color: red"
Style objectstyle={{ ... }}:style="{ ... }"n/a
Style directiven/an/astyle:prop={value}
<style> element or blockreportednot readnot read
SVG color attributesfill, stroke, ...samesame
Component identityexport namefile name, <CardTitle> or <card-title>file name, <Card.Title> or <Title>
Contractsby component namesame names as Reactsame names as React
Variants from cva / tvcomponent filecomponent file, or the barrel beside itcomponent file, incl. <script module>
Variants from typed propsyesnono
Received class prop acceptedclassName paramprops.class from defineProps()class from $props()
Wrappers across filesclassName forwardingprops.class, v-bind="$attrs", single-root fallthroughclassName and {...rest} forwarding
Base UI render propyesn/an/a
Dynamic element<Comp> from a variable<component :is><svelte:element>
Suggestions rewrite sourceyesyesyes
require-static-classesyesyesyes
no-unknown-classesyesyesyes
Project without components.jsonyes, componentImportsyes, componentImportsyes, componentImports
ESLintyesyes, vue-eslint-parseryes, svelte-eslint-parser
Oxlintyesscript blocks only, warns oncescript blocks only, warns once

Dynamic elements get the token rules but not no-restyle. <style> blocks in .vue and .svelte files are plain CSS; use a CSS linter for them.

Settings

Use settings.shadcn to configure component imports, class functions, and guidance shared across rules.

You don’t need shadcn/ui to use @shadcn/lint. It works with your own Tailwind components and theme.

shadcn/ui projects get automatic component and theme discovery via components.json.

For a custom setup, add settings at the root of .oxlintrc.json. Include only the settings you need:

{
  "settings": {
    "shadcn": {
      "ui": "@/ds",
      "componentImports": ["^@acme/ui(/|$)"],
      "ignoreImports": ["^@acme/ui/internal(/|$)"],
      "mergeFunctions": ["customMerge"],
      "variantFunctions": ["variants"],
      "note": "See DESIGN.md for design rules and approved exceptions."
    }
  }
}

For ESLint, add the same settings object to the config object containing your rules.

SettingWhat it does
uiRecognizes component imports by prefix. @/ds matches @/ds and @/ds/button, but not @/dsx.
componentImportsRecognizes component imports using regex patterns. Use it for additional directories or packages.
ignoreImportsSkips component recognition for imports matching these regex patterns. Takes precedence over recognition.
mergeFunctionsAdds functions whose arguments contain classes, such as customMerge("mt-4", "w-full").
variantFunctionsAdds functions whose object values contain classes.
noteAppends your text to every rule's error or warning.

All settings except note accept a string or an array of strings. note accepts a string.

The built-in class functions are cn, cx, clsx, cva, tv, twMerge, twJoin, and classNames. The built-in variant functions are cva and tv. Your function lists add to these defaults.

A recognition option set on a rule overrides its shared setting. ui prefixes always apply alongside componentImports. Recognition settings do not apply to no-inline-styles; note applies to every rule.

When you change the component directory, update the setup's directory override too, for example src/ds/**. See rule options for more examples.

Monorepos

Use your workspace package's import prefix for shared components. For a UI package in packages/ui, add this to the root .oxlintrc.json:

{
  "jsPlugins": ["@shadcn/lint"],
  "settings": {
    "shadcn": {
      "ui": "@workspace/ui/components"
    }
  },
  "rules": {
    "shadcn/no-restyle": ["error", { "allow": ["layout"] }]
  },
  "overrides": [
    {
      "files": ["packages/ui/src/components/**"],
      "rules": { "shadcn/no-restyle": "off" }
    }
  ]
}

Apps can use the shared components:

import { Button } from "@workspace/ui/components/button"

export function SaveButton() {
  return <Button className="w-full">Save changes</Button>
}

For ESLint, use the same settings and rules in the setup above, and change the component-directory override to packages/ui/src/components/**. These paths assume your lint config is at the workspace root.

The linter resolves components through your apps' TypeScript paths and package exports. If each app's components.json already points to the shared UI package, you can omit settings.shadcn.ui. Each app keeps its own theme configuration.

Documentation

See the documentation for rule examples, configuration, troubleshooting, and evals.

Contributing

Please read the contributing guide.

License

Licensed under the MIT license.

agents
ai
design
design-system
design-tools
shadcn
shadcn-ui
tailwindcss

Significant stargazers

(top 24 of 71)

Duncan Ogilvie

3,960 followers · starred Sep 2026

Travis Fischer

4,596 followers · starred Sep 2026

0xflotus

496 followers · starred Sep 2026

Danila Poyarkov

358 followers · starred Sep 2026