Claude Media
Claude CodeでTailwindのクラスを整理する — 並びと重複を編集後hookで直す

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 0

chmod +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の整形と二重になり、差分が増えます。整形はツールの担当だと、ここで明示します。

矛盾の報告が出たときの切り分けは、次の順で見ると早く片付きます。

切り分け

矛盾の報告が出たときの切り分け

  1. Q接頭辞(md: や hover: など)も同じクラス同士か?
    答えがいいえのとき

    接頭辞が違うなら矛盾ではなく、画面幅や状態ごとの指定です。ルールの対象かどうかを no-conflicting-classes のドキュメントで確かめてください。

    答えがはい
  2. 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つが抜けると、整形も検査も、一部のクラスだけ素通りします。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks-guide
  2. code.claude.com/docs/en/hooks
  3. github.com/tailwindlabs/prettier-plugin-tailwindcss
  4. github.com/schoero/eslint-plugin-better-tailwindcss
この記事を共有:XはてブLinkedIn