# @shadcn/lint
**Write design system rules that agents can verify.**
`@shadcn/lint` is an [agent-first linter](#built-for-agents) 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](#quickstart)
- [TypeScript vs @shadcn/lint](#typescript-vs-shadcnlint)
- [Built for agents](#built-for-agents)
- [Get started](#get-started)
- [Rules](#rules)
- [Frameworks](#frameworks)
- [Configuration](#settings)
## Quickstart
Give your coding agent this prompt:
```text
Read https://github.com/shadcn-ui/lint/blob/main/SETUP.md
and set up @shadcn/lint in this project.
```
Once installed, [choose your rules](#rules) and configure what’s allowed
in your design system.
Prefer to configure it yourself? See [Get started](#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`.
```tsx
Submit
```
```text
TS2353: Object literal may only specify known properties, and 'padding' does not exist in type 'Pick'.
```
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**:
```tsx
Submit
```
```text
"p-4" is not allowed on : 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.**
```js
"shadcn/no-restyle": ["error", {
allow: ["layout"],
contracts: [
{ pattern: "^Button$", allow: ["w-full", "mt-*", "mb-*"] },
],
}]
```
```tsx
// Allowed: use a size and let the page control placement and full width.
// Error: you are not allowed to change padding and shape.
// Error: you are not allowed to set a custom height or fixed width.
```
**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.
```js
"shadcn/no-restyle": ["error", {
allow: ["layout"],
contracts: [
{
pattern: "^CardTitle$",
allow: ["layout", "typography"],
deny: ["font-*"],
},
{ pattern: "^CardContent$", allow: ["layout", "spacing"] },
],
}]
```
```tsx
// Allowed: titles can change text size; content can change padding.
// Error: you are not allowed to change the title’s font weight.
// Error: you are not allowed to change the content’s typography.
```
**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.
```js
"shadcn/no-restyle": ["error", {
allow: ["layout"],
contracts: [
{ pattern: "^CardContent$", allow: ["layout", "spacing"] },
],
}],
"shadcn/no-arbitrary-values": "error",
```
```tsx
// Allowed: padding uses the theme’s spacing scale.
// Error: you are not allowed to use an arbitrary padding value.
```
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](#custom-messages) and [contracts](#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:
| Model | Completed tasks | Errors before | Errors after |
| ------------- | --------------: | ------------: | -----------: |
| Sonnet 5 | 8/8 | 69 | 0 |
| Haiku 4.5 | 8/8 | 66 | 0 |
| Opus 5 | 8/8 | 42 | 0 |
| GPT 5.6 Terra | 8/8 | 117 | 0 |
| GPT 5.6 Sol | 6/8 | 98 | 0 |
### 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](https://github.com/shadcn-ui/lint/blob/main/docs/evals.md) for results and methodology.
## Why a linter?
A linter is [programmable](#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.
```js
"shadcn/no-restyle": ["error", {
allow: ["layout"],
message: {
spacing: "Use the size prop instead of padding.",
},
}]
```
When an agent writes:
```tsx
Save changes
```
It sees:
```text
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:
```js
"shadcn/no-restyle": ["error", {
allow: ["layout"],
message: {
spacing: "Use a {{component}} size: {{sizes}}.",
},
}]
```
For a Button with `sm` and `lg` sizes, the error becomes:
```text
Use a Button size: sm, lg.
```
You can also tell agents where to find theme colors. For example, in
`no-raw-colors`:
```js
message: "Use a theme color from {{file}}."
```
If your theme is in `src/index.css`, the error becomes:
```text
Use a theme color from src/index.css.
```
See all [message placeholders](https://github.com/shadcn-ui/lint/blob/main/docs/rules.md#your-own-words).
### Contracts
Give each component its own rules. For example, let pages change a card
title's typography:
```js
"shadcn/no-restyle": ["error", {
allow: ["layout"],
contracts: [
{ pattern: "^CardTitle$", allow: ["layout", "typography"] },
],
}]
```
```tsx
// Allowed by the contract.
Account settings
// Reported: the contract does not allow color overrides.
Account settings
```
See [contracts and custom messages](https://github.com/shadcn-ui/lint/blob/main/docs/design-systems.md).
## Get started
Requires Node.js 20.19 or later and a version supported by your linter.
Bun 1.3 or later works too, with `bun lint` or `bun --bun lint`.
### React
#### ESLint
Requires ESLint 9.30 or later.
```bash
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.
```js
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",
},
},
])
```
```bash
npx eslint .
```
#### Oxlint
Requires Oxlint 1.80 or later.
```bash
npm install -D @shadcn/lint oxlint
```
Create `.oxlintrc.json`:
```json
{
"jsPlugins": ["@shadcn/lint"],
"rules": {
"shadcn/no-arbitrary-values": "error"
}
}
```
```bash
npx oxlint
```
### Vue
#### ESLint
Requires ESLint 9.30 or later.
```bash
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.
```js
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",
},
},
])
```
```bash
npx eslint .
```
#### Oxlint
Requires Oxlint 1.80 or later. Oxlint reads the `