← レポート一覧
SHADCN-UI / LINT / ELI5

@shadcn/lint: エージェントが書く UI を
デザインシステムに合わせるリンター

Tailwind のデザインシステムで「何を変えてよいか」を決めておき、はみ出したときに直し方までエラーで伝えるツールです。

1 / どんなツールか

どんなツールか

@shadcn/lint は、Tailwind v4 のデザインシステム向けのリンターです。ESLint と Oxlint のプラグインとして動きます。名前に shadcn とありますが、shadcn/ui を使っていないプロジェクトでも使えます

README では「agent-first」、つまり UI を書くコーディングエージェントのために作ったと説明されています。

1 エージェントが UI を書く <Button className="p-4"> 2 lint を実行する npm run lint 3 エラーに直し方が書いてある p-4 は Button では使えない。 size(sm, lg)を使う。 4 デザインシステムの中で直す <Button size="lg">

実際のエラーは次のような文面です(README の画像より)。何がだめかに加えて、代わりに何を使えばよいか、どのファイルを見ればよいかまで書かれています。

"p-4" is not allowed on <Button>: <Button> owns its spacing. Use a variant (sm, lg) for size. Use margin for layout or a gap on the parent for space around it. Add a size in components/ui/button.tsx only if the design explicitly calls for one.
決まりを守らせるだけでなく、あなたのコンポーネント・バリアント・テーマから直し方を提案するのが特徴です。
2 / なぜリンターか

型ではなくリンターで決める理由

「Button の margin と width は外から変えてよいが、padding は Button が決める」という決まりを考えます。TypeScript でも、style の型を Pick<CSSProperties, "margin" | "width"> に絞れば守らせることはできます。

TypeScript の場合

だめなことだけがわかる

TS2353: ... 'padding' does not exist in type 'Pick<CSSProperties, "margin" | "width">'.

padding が使えないことはわかりますが、Button の大きさをどう変えればよいかは書かれていません。

@shadcn/lint の場合

代わりに使うものまでわかる

"p-4" is not allowed on <Button>: <Button> owns its spacing. Use a size (sm, lg), or margin here or gap on the parent ...

size の候補、margin や親の gap という別の置き場所、コンポーネントのファイルまで示します。

もう1つの理由は、リンターの設定はコンポーネントのコードを変えずに書けることです。複雑な型を作る必要もありません。

同じ部品、別の決まり

プロジェクトごとにルールを変えられる

コンポーネントのコードはそのままで、プロジェクトごとに許す範囲を決められます。

自分のものでない部品

外部パッケージにもかけられる

サードパーティのコンポーネントにも、フォークやラッパーなしでルールを当てられます。

共通の設定

複数プロジェクトで共有できる

デザインシステムの共通設定を置き、各プロジェクトが自分のルールを足せます。

3 / しくみ

リンターが読んでいるもの

リンターはアプリを動かさず、ソースコードだけを読みます。そのとき、次の4つを材料にしてエラーと提案を作ります。

コンポーネント import をたどって探す テーマのトークン @theme の --color-* など Tailwind v4 本体 CSS が作られるクラスか バリアント cva・tv・props の型 あなたの .tsx className・style など @shadcn/lint アプリは動かさない どのクラスがだめで、代わりに何を使うか
コンポーネント
components.json が指す UI ディレクトリから探し、@/、tsconfig の paths、re-export や別名の import もたどります。ファイルがなければ components/ui などを見ます。
テーマのトークン
@theme--color-* を読みます。bg-brand は宣言済みなので通り、bg-zinc-100(Tailwind の既定パレット)や未宣言の bg-highlight は指摘されます。色は OKLab で比べて近いトークンを提案します。
Tailwind v4 本体
インストール済みの Tailwind に、そのクラスで CSS が生成されるかを問い合わせます。hovr:flex のような打ち間違いがわかります。
バリアント
cvatv の定義と、size?: "sm" | "lg" のような文字列ユニオンの props を読み、提案に使います。

クラスは className だけでなく、cnclsxcva などの呼び出し、同じファイル内の変数(1段まで)、className を受け渡すラッパーコンポーネントもたどって調べます。

4 / ルール

6つのルール

本番のデザインシステムを調べ、コーディングエージェントで試しながら作られたルールです。それぞれ warnerror で有効にします。

no-restyle
デザインシステムのコンポーネントに className で見た目を上書きすること。
指摘 <Button className="p-4 bg-pink-500">
no-raw-colors
テーマにない色。Tailwind のパレット色、未宣言のトークン、SVG の fill="#ec4899" など。
指摘 bg-pink-500 通る bg-primary
no-arbitrary-values
任意の値。同じ値がスケールにあればそれを提案します。
指摘 p-[13px]p-3.25 を提案
no-inline-styles
インラインの style<style> 要素。動的な値は CSS のカスタムプロパティで渡します。
no-unknown-classes
Tailwind が CSS を作れないクラス。
指摘 rounded-huge flex-cols
require-static-classes
リンターが読めないクラスの値。読めないと他のルールで調べられないためです。
指摘 className={props.tone}
アプリのページ側 app/ や features/ など no-restyle no-arbitrary-values require-static-classes コンポーネントの中 components/ui/ など。自分で見た目を決める no-restyle: off no-arbitrary-values: off require-static-classes: off no-raw-colors と no-inline-styles は有効のまま
ドキュメントの推奨設定。コンポーネント自身は padding や ring-[3px] のような構造上の値を使うことがあるため
5 / 設定

何を許すかと、エラーの文面を決める

no-restyle は、クラスを色・文字・余白などのカテゴリに分けて判定します。基本の設定 allow: ["layout"] では、配置(margin や width)だけを外から変えてよいことになります。

コンポーネントごとに別の決まりを持たせたいときは contracts を使います。次の図は、カードのタイトルには文字の調整を、カードの中身には余白の調整を許した例です。

配置文字余白 効果動き mt-4bg-*text-smp-4 roundedshadowanimate 既定 allow: layout CardTitle + typography CardContent + spacing
塗りの丸が許可、白抜きの丸は指摘される。README の contracts の例を表にしたもの
// eslint.config.mjs または .oxlintrc.json の rules "shadcn/no-restyle": ["error", { allow: ["layout"], contracts: [ { pattern: "^CardTitle$", allow: ["layout", "typography"], deny: ["font-*"] }, { pattern: "^CardContent$", allow: ["layout", "spacing"] }, ], }]

contract は書いたキーだけを置き換え、残りは上の設定を引き継ぎます。allow を書くときは、配置を残したければ layout も並べる必要があります。

エラーの文面も差し替えられます。{{sizes}}{{variants}}{{file}} などのプレースホルダーには、実際のコンポーネントの値が入ります。

設定
message: { spacing: "Use a {{component}} size: {{sizes}}.", }
Button に sm と lg があるときの表示
Use a Button size: sm, lg.

settings.shadcn.note を使うと、すべてのルールのエラーの末尾に「DESIGN.md を見て」のような一文を足せます。

6 / 効果

エージェントで試した結果

作者たちは 150 回を超えるタスク実行で試しています。README の表は、モデルごとに1回分の実行で、lint のフィードバックを受ける前と後のエラー数を比べたものです。

フィードバック前のエラー数 Sonnet 5Haiku 4.5Opus 5 GPT 5.6 TerraGPT 5.6 Sol 8/88/88/8 8/86/8 696642 11798 00000
README の表を図にしたもの。モデル名の横の数字は完了したタスク数(8 タスク中)

ルールの説明文だけを渡して自分で見直させる場合と、lint のエラーを渡す場合も比べています。Claude の3モデルでは、エラーを渡したほうが修正のコストが 10〜48% 低く、全タスクが1回の修正で通りました。

モデルルール文のみエラーを渡す修正コストの差
Sonnet 5$3.57$2.47約31%減
Haiku 4.5$1.41(7/8 通過)$0.74(8/8)約48%減
Opus 5$4.35$3.93約10%減

差が大きいのは小さいモデルです。Haiku はルール文だけだと、間違ったトークン名を3回見直しても見つけられませんでしたが、そのクラスを名指しするエラーを渡すと1回で直しました。

修正前

ピンクのボタンを直接塗る

<Button className="bg-[#FF6B35] text-white hover:bg-[#FF6B35]/90">
lint を受けた修正後

トークンとバリアントを足す

<Button variant="brand">

brand トークンを宣言し、Button に brand バリアントを追加する形に落ち着くことが多かったそうです。eslint-disable やインラインスタイルでごまかした実行はありませんでした。

別のモデル(judge)で修正前後の見た目を比べると、ほぼすべての組が 10 点満点中 9〜10 点でした。見た目を捨ててエラーを消したわけではない、という確認です。

ただし、これは特定の実行を測った結果で、どのモデル・どのプロジェクトでも同じになる約束ではない、とドキュメント自身が書いています。evals の詳しい結果は Claude の3モデルが中心で、判定役のモデルも同じ系統です。
7 / 限界

見えないものと限界

lint が通っても、すべてのスタイルの経路を調べたことにはなりません。ドキュメントには、見えないものが具体的に挙げられています。

親のセレクタ

[&_button]:bg-primary

親から子のコンポーネントを塗る書き方は、no-restyle では追いません。

読めない props

<Button {...props}>

中身が読めないオブジェクトの展開は、そのままにされます。

import した値

ファイルをまたぐ値

クラスの値は同じファイル内だけをたどります。

素の CSS

globals.css@apply

スタイルシートの中は対象外です。CSS 用のリンターを使います。

新しいトークン

@theme への追加

新しく宣言したトークンは、仕様として許されます。

無効化コメント

eslint-disable

コメントでルールを外せます。理由の記載を必須にする設定を勧めています。

わざとすり抜けを試す7つの実験では、5つを検出し、2つがすり抜けました。

検出した
5つ CSS カスタムプロパティ経由の生の16進色 / 変数に入れた動的なクラス文字列 / SVG の fill / ui コンポーネントの別名での再 export / これらの組み合わせ
すり抜けた
2つ globals.css に書いた素の CSS クラス / 新しいテーマトークンを作ること
新しいトークン・バリアント・contract・無効化コメントは、人がデザインの判断としてレビューするものです。lint は、新しい見た目がシステムに入るべきかまでは決められません。
8 / 導入

導入の手順

必要なもの: Node.js 20.19 以上、Tailwind v4、ESLint 9.30 以上または Oxlint 1.80 以上(Oxlint の JS プラグイン API はまだ alpha)。npm の @shadcn/lint は 2026-09-15 時点で 0.1.0、ライセンスは MIT です。

1
エージェントにセットアップを頼む
README の Quickstart は、エージェントに次の文を渡す方法です。SETUP.md の指示では、プラグインの導入と登録だけを行い、ルールは有効にしません。どのルールで何を許すかは利用者が決めます。
Read https://github.com/shadcn-ui/lint/blob/main/SETUP.md and set up @shadcn/lint in this project.
2
自分で入れる場合は、まず no-restyle から
npm install -D @shadcn/lint oxlint
// .oxlintrc.json { "jsPlugins": ["@shadcn/lint"], "rules": { "shadcn/no-restyle": ["error", { "allow": ["layout"] }] } }
3
AGENTS.md に1行足す
lint のコマンドを package.jsonlint スクリプトにしたうえで、エージェントへの指示に書きます。
After making changes, run `npm run lint` and fix all errors.
4
既存のプロジェクトは warn から少しずつ
最初は warn にして、多いパターン(ボタンの padding など)から直します。警告の数を --max-warnings 287 のように CI で上限にし、直すたびに下げます。新しいコードのフォルダだけ先に error にすることもできます。ルールがきれいになったら error に上げ、他のルールを足していきます。
9 / まとめ

まとめ

1
デザインシステムの決まりを、エージェントが自分で確かめられる形にするリンター
Tailwind v4 のプロジェクト向け。ESLint と Oxlint で動き、shadcn/ui は必須ではありません。
2
エラーに、プロジェクト自身のバリアント・サイズ・トークンを使った直し方が入る
contracts でコンポーネントごとに許す範囲を決め、メッセージも自分たちの言葉にできます。
3
試した実行ではエラーがほぼ1回でゼロになったが、見えない経路もある
素の CSS や新しいトークンは対象外です。新しいトークンやバリアントは人がレビューします。
出典と用語メモ
用語メモ / デザインシステム=色・余白・部品の使い方などの決まりと、それを形にしたコンポーネントの集まり。shadcn/ui=コードを自分のプロジェクトにコピーして使う形の React コンポーネント集。トークン=色や角丸などに名前を付けた値(例: --color-primary)。バリアント=コンポーネントに用意された見た目の種類(例: variant="destructive")。cva・tv=バリアントを定義するためのライブラリ(class-variance-authority、tailwind-variants)。Oxlint=Rust 製の高速な JavaScript/TypeScript リンター。
← レポート一覧