Claude CodeでTailwindのクラスを整理する — 並びと重複を編集後hookで直す
Claude Codeが書いたTailwindのクラスを、編集のたびにprettier-plugin-tailwindcssで並べ替え、重複と矛盾はESLintで検出してClaudeに直させる構成を、hookの設定つきで書きます。
Claude CodeにTailwindでUIを書かせると、クラスの並びが毎回違い、同じクラスが二度書かれ、p-4 と p-2 のように食い違うクラスが同居することがあります。並びと重複は機械で直せます。矛盾したクラスのどちらを残すかは、機械では決まりません。
この記事では、編集のたびに走るPostToolUse hookで、直せるものはツールに直させ、直せないものだけをClaudeに差し戻す構成を作ります。使うのは prettier-plugin-tailwindcss と eslint-plugin-better-tailwindcss の2つです。
どのツールがどの問題を引き受けるか
結論を先に置きます。並びはPrettierのプラグイン、矛盾と未知のクラスはESLintのプラグインが検出します。そして検出だけで直せない分がClaudeの仕事になります。
| 問題 | 担当 | 自動で直るか |
|---|---|---|
| クラスの並びがばらばら | 担当prettier-plugin-tailwindcss | 自動で直るか直る(整形時に並べ替え) |
| 同じクラスが重複している | 担当prettier-plugin-tailwindcss | 自動で直るか直る(重複を取り除く) |
| 同じCSSプロパティを触るクラスが同居 | 担当eslint-plugin-better-tailwindcssの no-conflicting-classes | 自動で直るか直らない(報告のみ) |
| Tailwindに登録されていないクラス | 担当同 no-unknown-classes | 自動で直るか直らない(報告のみ) |
PrettierプラグインのREADMEは、設定した属性に加え、class、className、:class、[ngClass]、@apply を既定で並べ替えると説明しています。重複の除去も既定の動作です。READMEには、重複を消すとFluidやBladeのようなテンプレート言語で困る場合に tailwindPreserveDuplicates で止められるとあります。
ESLintプラグインの側は、表の autofix 列が決め手です。no-conflicting-classes と no-unknown-classes は、どちらも自動修正の列が空です。報告だけで終わるので、直す手を別に用意する必要があります。その手がClaudeです。
「未使用クラスの検出」を期待して来た方には、先にずらしておく点があります。Tailwindは使われているクラスを拾って出力する仕組みなので、CSSに溜まった未使用クラスを掃除する道具ではありません。ESLintプラグインが見るのは、その逆の「Tailwindに存在しないクラス」です。両ツールのREADMEには、未使用クラスを検出するルールの記載がありません。
Prettierのプラグインを入れる
導入は2行です。READMEの手順どおり、開発依存として入れてからプラグインを登録します。
npm install -D prettier prettier-plugin-tailwindcss{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/global.css",
"tailwindFunctions": ["clsx", "cn"]
}この設定の要点は3つあります。
- Tailwind CSS v4では、テーマやカスタムユーティリティを含むCSSのエントリポイントを
tailwindStylesheetで指定します。v3ではtailwindConfigでconfigファイルを指します clsx()のような関数呼び出しの中の文字列は、tailwindFunctionsに関数名を挙げて初めて並べ替えの対象になります。shadcnのcnを使う場合も同じく挙げます- 他のPrettierプラグインと併用する場合、このプラグインは配列の最後に置く必要があります。READMEが「MUST come last」と明記しています
Claudeが cn("flex gap-2", isActive && "bg-blue-500 flex") のような書き方をするプロジェクトでは、tailwindFunctions を入れ忘れると、属性内のクラスだけが整い、関数内のクラスが取り残されます。
ESLintは矛盾と未知のクラスだけに絞る
eslint-plugin-better-tailwindcss は、整形系(stylistic)と正しさ系(correctness)の2系統のルールを持ちます。READMEによると、設定済みのセットは stylistic、correctness、recommended の3つで、既定では整形系が警告、正しさ系がエラーとして報告されます。
並びの面倒はPrettierが見るので、ここでは正しさ系だけを使います。recommended には並び順を揃える enforce-consistent-class-order と、行の折り返しを揃える enforce-consistent-line-wrapping が含まれています。Prettierと担当が重なるので、correctness を選ぶのが素直です。
TSXの設定例は、プラグインのtsx向けドキュメントの形に沿うと次のようになります。
// eslint.config.js
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
import { defineConfig } from "eslint/config";
import { parser as eslintParserTypeScript } from "typescript-eslint";
export default defineConfig([
{
extends: [eslintPluginBetterTailwindcss.configs.correctness],
settings: {
"better-tailwindcss": {
entryPoint: "src/global.css",
},
},
},
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
parser: eslintParserTypeScript,
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
]);entryPoint はTailwind v4のCSSエントリポイント、v3なら tailwindConfig にconfigファイルのパスを書きます。Prettierの tailwindStylesheet と同じファイルを指しておくと、両ツールが同じテーマを見ます。公式のtsx向け例には、型情報付きの解析のための parserOptions.project も載っています。型情報付きの解析が要る場合は、プロジェクトの tsconfig に合わせて足してください。
押さえておく制約が2つあります。
no-conflicting-classesが対応するのはTailwind v4だけです。ルール表のv3の列が空になっています。v3のプロジェクトでは、矛盾の検出はこの構成では得られませんno-unknown-classesは、自分で定義したcardやbtnのようなコンポーネントクラスも未知として報告します。ルールのドキュメントにあるdetectComponentClassesをtrueにすると、そうしたクラスを報告から外せます
編集後hookに組み込む
土台になるのは、Claude Codeのhooksガイドにある「編集のたびにPrettierを走らせる」例です。PostToolUse に Edit|Write をmatcherとして置き、tool_input.file_path からファイルを取り出します。ガイドのコマンドはPrettierを実行するだけなので、ESLintの検査と差し戻しを足したスクリプトに置き換えます。
.claude/hooks/tailwind-tidy.sh を作ります。
#!/bin/bash
f=$(jq -r '.tool_input.file_path // empty')
# Tailwindのクラスを書く拡張子だけ処理する
case "$f" in
*.tsx|*.jsx|*.vue|*.svelte|*.astro|*.html) ;;
*) exit 0 ;;
esac
# 1. 並べ替えと重複除去
npx prettier --write "$f" >/dev/null 2>&1
# 2. 矛盾と未知のクラスを検査
out=$(npx eslint "$f" 2>&1) || {
echo "Tailwindのクラスに問題があります。意図を読んで直してください。" >&2
echo "$out" >&2
exit 2
}
exit 0chmod +x .claude/hooks/tailwind-tidy.sh で実行権限を付け、.claude/settings.json に登録します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/tailwind-tidy.sh"
}
]
}
]
}
}仕組みの肝は終了コードです。hooksのリファレンスによると、PostToolUseの終了コード2は、ツールがすでに実行された後でも、stderrをClaudeに見せます。終了コード0で終えたhookのstderrは、Claudeには見えません。だからスクリプトは、ESLintが何も報告しなければ0、報告があれば内容をstderrへ書いて2で終わる形にしています。
Prettierの整形は黙って行われ、会話には何も出ません。hooksガイドも、成功したhookは何も表示しないと書いています。整形されたかどうかはファイルの中身で確かめます。
手で動作を確かめる
Claudeに任せる前に、hookのスクリプトを単体で動かします。hooksガイドが勧めるとおり、サンプルのJSONを流し込み、終了コードを見ます。
echo '{"tool_input":{"file_path":"src/Button.tsx"}}' \
| .claude/hooks/tailwind-tidy.sh
echo $?意図的に className="p-4 flex p-2" と書いたファイルで試すと、Prettierが並べ替えと重複除去を行い、矛盾が残ればESLintの報告が出て 2 が返ります。報告には no-conflicting-classes などのルール名と該当行が出て、終了コードは 2 になります。この2つが揃えば、hookは期待どおり動いています。手元で出た報告は、次の節のルールの材料にしてください。
差し戻された矛盾を、Claudeにどう直させるか
hookが返すのは「矛盾がある」という事実だけです。p-4 と p-2 のどちらが正しいかは、書いた意図にしか答えがありません。ここを黙って片方消させると、見た目が変わります。
CLAUDE.mdに、直し方の方針を書いておくと安定します。
## Tailwindのクラス
- 編集後のhookが矛盾(conflicting)を報告したら、周囲のレスポンシブ接頭辞と
状態接頭辞を見て、意図した方を残す。判断できないときは両方を残さず質問する
- 未知のクラス(unknown)を報告されたら、綴りの誤りか、未定義のコンポーネント
クラスかを先に調べる。報告を黙らせるためにルールを無効化しない
- クラスの並び順は手で直さない。Prettierが整える3つ目が効きます。Claudeが並びまで手で直そうとすると、Prettierの整形と二重になり、差分が増えます。整形はツールの担当だと、ここで明示します。
矛盾の報告が出たときの切り分けは、次の順で見ると早く片付きます。
矛盾の報告が出たときの切り分け
- Q接頭辞(md: や hover: など)も同じクラス同士か?答えがいいえのとき答えがはい
接頭辞が違うなら矛盾ではなく、画面幅や状態ごとの指定です。ルールの対象かどうかを
no-conflicting-classesのドキュメントで確かめてください。 - Q同じ条件で両方が同時に効く書き方か?答えがいいえのとき答えがはい
cn()やclsx()の引数で、条件によって一方だけが効く書き方は、同居していても実際には衝突しません。片方に寄せずに、条件の構造のまま残します。
本当に同じ要素で衝突している
意図した値を一つ残し、もう一方を消します。どちらか判断できないときはClaudeに質問させます。
ハマりやすい点
Bashで書き換えたファイルはhookに届かない
Edit|Write のmatcherが反応するのは、ClaudeのEditツールとWriteツールが書いたときだけです。ClaudeがBashで sed -i を使ってクラスを置き換えた場合、PostToolUseのhookは動きません。hooksガイドも、Bashのコマンドや外部のプロセスが同じファイルを書き換えた場合は FileChanged を使うよう案内しています。この点の回避は、PostToolUse hookのLint自動修正でも扱っています。
型情報付きのESLintは遅い
parserOptions.project を使うTypeScriptの設定では、ファイル1つの検査でもプロジェクト全体の型情報を読むため、編集のたびに時間がかかることがあります。command hookの既定の制限時間は10分ですが、Claudeの作業を待たせる時間でもあります。timeout を短く設定するか、このhook用に型情報を使わないESLint設定を分けて持つ選択肢があります。
古い構成からの移行
ESLintの設定がまだ .eslintrc 形式なら、先にflat configへ移すと、上の設定例がそのまま使えます。手順はESLintのflat config移行の記事にあります。Tailwindのv3からv4へ上げる途中なら、v4移行の記事の順で先に上げてから、tailwindStylesheet と entryPoint を揃えてください。
このhookが向かない場面
この構成は、Claudeがファイルを直接編集する流れに合わせています。CIで全ファイルを一括検査するなら、同じルールを npx eslint . と npx prettier --check . でCIにも置き、hookは手元の即時フィードバックに回す分担が自然です。hookは「その場で気づかせる」道具で、「全体を保証する」道具ではありません。
Claude Codeのhooks全体の仕組みはClaude Code Hooks完全ガイドにまとまっています。
まとめ
並びと重複は、Prettierのプラグインが編集のたびに黙って直します。矛盾と未知のクラスは、ESLintが見つけ、直す判断をClaudeに回します。この分担の要は、自動で直せるものをClaudeの会話に出さないことです。出てくるのは、意図を読まないと直せない指摘だけになります。
まずは tailwindFunctions と entryPoint の指定漏れを疑ってください。この2つが抜けると、整形も検査も、一部のクラスだけ素通りします。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。