Claude CodeでTailwind CSSを使いこなす実践Tips:崩れにくいUI改善ガイド
Claude CodeとTailwind CSSで崩れにくいUIを作る実践Tips。設計、コード例、落とし穴、検証手順を解説。
Tailwind CSSは、p-4、grid、text-smのような小さなユーティリティクラスを組み合わせてUIを作るCSSフレームワークです。Claude Codeに「いい感じにして」と頼むだけでも画面は変わりますが、そのままだとクラスが増えすぎたり、スマホだけ崩れたり、ダークモードで読みにくくなったりします。
この記事では、初心者でもそのまま試せる形で、Claude CodeにTailwind CSSの実装を任せるときの実践Tipsをまとめます。扱う範囲は、デザイントークン、レスポンシブユーティリティ、コンポーネント抽出、class soup(長すぎて読めないclassName)、ダークモード、フォーム・ボタン・カード、safelistとcontent scanning、Playwrightによる見た目チェックです。
公式情報は、Tailwind CSSのTheme variables、Responsive design、Dark mode、Detecting classes in source files、Adding custom stylesを基準にしています。Reactの型付きコンポーネントはReactのTypeScriptガイド、Claude Codeの基本はClaude Code公式ドキュメント、見た目の回帰テストはPlaywrightのVisual comparisonsを参照してください。
Claude Codeの頼み方そのものを整えたい場合は、先にプロンプト改善Tipsを読むと、このあと紹介する依頼文の意図がつかみやすくなります。アプリ全体のモバイル体験まで広げたい場合は、PWA実装ガイドも相性が良いです。
まずClaude Codeに任せる範囲を決める
Claude Codeはコードベースを読んでファイル編集やコマンド実行まで進められるエージェント型の開発ツールです。ただし、デザインの正解を自動で保証するわけではありません。特にTailwind CSSはクラスがHTMLやJSXに直接出るため、方針を渡さないと「動くけれど保守しづらいUI」になりがちです。
最初に、どの画面を改善するのか、既存の色・余白・角丸を変えてよいのか、スマホを優先するのか、購入や問い合わせのCTAを邪魔していないかを明文化します。MasaがこのサイトのUI改善で失敗したときは、ヘッダーだけを見て調整した結果、記事下のCTAボタンと広告枠の余白が狭くなり、スマホで押しづらくなりました。Tailwindのクラスは局所的に見えて、実際には導線全体に影響します。
Claude Codeへの最初の依頼は、実装ではなく棚卸しにします。
このリポジトリのTailwind CSS利用状況を確認してください。
まだ編集しないで、次の観点でレポートしてください。
- デザイントークンとして共通化されている色、余白、角丸、影
- classNameが長すぎるReactコンポーネント
- スマホ、タブレット、PCで崩れそうなレイアウト
- ダークモード対応済み/未対応の箇所
- フォーム、ボタン、カードで重複しているスタイル
- Tailwindのcontent scanningやsafelistで事故りそうな動的クラス
- 見た目確認に使える既存のテスト、Storybook、Playwright設定
この段階で「どのファイルを触るべきか」を出させてから、1画面ずつ進めると手戻りが減ります。
デザイントークンを先に決める
デザイントークンとは、色、余白、フォント、角丸、影などの小さな設計値に名前を付けたものです。Tailwind CSS v4系では@themeでテーマ変数を定義すると、bg-brand-600やrounded-cardのようなユーティリティとして使えるようになります。既存プロジェクトがtailwind.config.ts中心なら、同じ考え方をtheme.extendへ写せば大丈夫です。
次のCSSは、ブランドカラー、画面幅、カードの角丸、影、フォントを最小限のトークンとして定義する例です。@custom-variant darkは、HTMLに.darkクラスを付けたときにdark:ユーティリティを効かせる設定です。
/* src/styles/app.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--font-sans: Inter, "Noto Sans JP", system-ui, sans-serif;
--color-brand-50: #eef6ff;
--color-brand-100: #d9ebff;
--color-brand-600: #2563eb;
--color-brand-700: #1d4ed8;
--color-ink: #111827;
--color-muted: #6b7280;
--color-surface: #ffffff;
--color-surface-soft: #f8fafc;
--color-danger: #dc2626;
--radius-card: 0.75rem;
--shadow-card: 0 16px 40px rgb(15 23 42 / 0.08);
--breakpoint-3xl: 112rem;
}
Claude Codeには「色を青っぽく」ではなく、「既存のbrandトークンを使い、必要なら@themeに追加してから実装して」と頼みます。こうすると、画面ごとにbg-blue-600、bg-sky-600、bg-indigo-600が混ざる事故を避けやすくなります。
レスポンシブはmobile-firstで読む
Tailwindのレスポンシブユーティリティは、基本のクラスをモバイルに置き、sm:、md:、lg:のような接頭辞で広い画面の差分を足します。初心者がやりがちな失敗は、PC画面を先に見てgrid-cols-4を付け、そのあとスマホで崩れてから直すことです。
Claude Codeへ依頼するときは、画面幅ごとの期待値を明示します。
商品カード一覧をTailwind CSSで改善してください。
条件:
- 375pxでは1列、640px以上で2列、1024px以上で3列
- 画像は常に正方形で、カードの高さがそろう
- CTAボタンはカード下部に揃える
- 既存のProductCardのprops型は変えない
- 変更後にスマホ幅とPC幅でスクリーンショット確認する
実装では、親グリッドとカード内部の責務を分けます。
type Product = {
id: string;
name: string;
price: number;
imageUrl: string;
badge?: string;
};
export function ProductGrid({ products }: { products: Product[] }) {
return (
<section className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</section>
);
}
function ProductCard({ product }: { product: Product }) {
return (
<article className="flex h-full flex-col overflow-hidden rounded-card border border-slate-200 bg-surface shadow-card dark:border-slate-800 dark:bg-slate-950">
<div className="relative aspect-square overflow-hidden bg-surface-soft">
<img
src={product.imageUrl}
alt={product.name}
className="h-full w-full object-cover transition duration-200 hover:scale-105"
/>
{product.badge ? (
<span className="absolute left-3 top-3 rounded-full bg-brand-600 px-3 py-1 text-xs font-semibold text-white">
{product.badge}
</span>
) : null}
</div>
<div className="flex flex-1 flex-col p-4">
<h3 className="line-clamp-2 text-base font-semibold text-ink dark:text-white">
{product.name}
</h3>
<p className="mt-2 text-sm text-muted dark:text-slate-400">
税込 {product.price.toLocaleString("ja-JP")} 円
</p>
<button className="mt-auto rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-brand-600 focus:ring-offset-2 dark:focus:ring-offset-slate-950">
詳細を見る
</button>
</div>
</article>
);
}
mt-autoでボタンを下に揃え、aspect-squareで画像比率を固定し、line-clamp-2で商品名の長さによる崩れを抑えています。Claude Codeにこの意図まで説明させると、次の修正でも同じ方針を守りやすくなります。
class soupはコンポーネント抽出で減らす
class soupとは、classNameが長くなりすぎて、何がデザインの意図で何が状態差分なのか読めなくなる状態です。Tailwind CSSでは小さなユーティリティを直接書くのが基本ですが、同じボタンが10回出てきたら抽出した方が保守しやすくなります。
@applyで全部CSSに逃がす前に、Reactコンポーネントとvariantのマップで整理するのが実務では扱いやすいです。クラス名がソースに文字列として残るため、Tailwindの検出にも引っかかりやすくなります。
import type { ButtonHTMLAttributes, ReactNode } from "react";
type ButtonVariant = "primary" | "secondary" | "danger";
const buttonVariants: Record<ButtonVariant, string> = {
primary: "bg-brand-600 text-white hover:bg-brand-700 focus:ring-brand-600",
secondary:
"border border-slate-300 bg-white text-slate-900 hover:bg-slate-50 focus:ring-slate-400 dark:border-slate-700 dark:bg-slate-900 dark:text-white",
danger: "bg-danger text-white hover:bg-red-700 focus:ring-danger",
};
function cn(...classes: Array<string | false | null | undefined>) {
return classes.filter(Boolean).join(" ");
}
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: ButtonVariant;
loading?: boolean;
children: ReactNode;
};
export function Button({
variant = "primary",
loading = false,
disabled,
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
"inline-flex min-h-10 items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold transition focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:ring-offset-slate-950",
buttonVariants[variant],
className,
)}
disabled={disabled || loading}
{...props}
>
{loading ? "処理中..." : children}
</button>
);
}
Claude Codeには「ボタンを共通化して」だけでなく、「既存の見た目を変えすぎず、variantはprimary、secondary、dangerに限定し、動的なTailwindクラス結合は避けて」と伝えます。bg-${color}-600のような組み立て方は、見た目が本番で消える原因になります。
フォームとカードは状態を先に並べる
フォーム、ボタン、カードは収益導線に直結します。問い合わせ、資料請求、購入リンク、無料チートシートのダウンロードなど、ユーザーが行動する場所ほど、hoverだけでなくfocus、disabled、error、dark modeを確認します。
次の例は、CTA付きの問い合わせフォームです。コピーしてReactコンポーネントとして使えるように、送信イベント、型、エラー表示、アクセシビリティ用のaria-describedbyを含めています。
"use client";
import type { FormEvent } from "react";
type LeadFormProps = {
onSubmit: (values: { email: string; message: string }) => void;
error?: string;
};
export function LeadForm({ onSubmit, error }: LeadFormProps) {
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
onSubmit({
email: String(formData.get("email") ?? ""),
message: String(formData.get("message") ?? ""),
});
}
return (
<form
onSubmit={handleSubmit}
className="space-y-4 rounded-card border border-slate-200 bg-white p-5 shadow-card dark:border-slate-800 dark:bg-slate-950"
>
<div>
<label htmlFor="email" className="text-sm font-medium text-slate-900 dark:text-white">
メールアドレス
</label>
<input
id="email"
name="email"
type="email"
required
aria-describedby={error ? "lead-form-error" : undefined}
className="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-brand-600 focus:ring-2 focus:ring-brand-600/20 dark:border-slate-700 dark:bg-slate-900 dark:text-white"
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="message" className="text-sm font-medium text-slate-900 dark:text-white">
相談したい内容
</label>
<textarea
id="message"
name="message"
rows={4}
className="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-brand-600 focus:ring-2 focus:ring-brand-600/20 dark:border-slate-700 dark:bg-slate-900 dark:text-white"
placeholder="TailwindのUI改善、Claude Code導入、レビュー体制など"
/>
</div>
{error ? (
<p id="lead-form-error" className="text-sm font-medium text-danger">
{error}
</p>
) : null}
<button className="w-full rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-brand-600 focus:ring-offset-2 dark:focus:ring-offset-slate-950">
相談する
</button>
</form>
);
}
このようなフォームは、Claude Codeに「見た目」だけでなく「フォーカスリングが見えること」「エラー文が入力欄と関連付くこと」「スマホ幅でボタンが押しやすいこと」までレビューさせます。アクセシビリティ観点を深掘りするならClaude Codeアクセシビリティ記事も内部リンクとして読ませると効果的です。
ダークモードは最後ではなく最初に入れる
ダークモードは、最後にdark:を足す作業ではありません。白背景、薄い枠線、影、画像の上の文字、エラー色、フォーカスリングが全部関係します。最初から.darkクラス方式にするのか、OS設定に合わせるのかを決めておきます。
以下は、ReactでHTML要素に.darkを付け替える最小例です。Tailwind側で@custom-variant dark (&:where(.dark, .dark *));を定義している前提です。
"use client";
import { useEffect, useState } from "react";
type Theme = "light" | "dark";
export function ThemeToggle() {
const [theme, setTheme] = useState<Theme>("light");
useEffect(() => {
document.documentElement.classList.toggle("dark", theme === "dark");
document.documentElement.style.colorScheme = theme;
}, [theme]);
return (
<button
type="button"
onClick={() => setTheme((current) => (current === "dark" ? "light" : "dark"))}
className="rounded-lg border border-slate-300 px-3 py-2 text-sm font-semibold text-slate-900 transition hover:bg-slate-50 dark:border-slate-700 dark:text-white dark:hover:bg-slate-900"
>
{theme === "dark" ? "ライトモード" : "ダークモード"}
</button>
);
}
落とし穴は、カードだけダーク対応して本文やフォームは未対応のままにすることです。Claude Codeには「この画面のすべての背景、文字、枠線、影、入力欄、focus状態をライト/ダークで確認して」と頼みます。
safelistとcontent scanningで本番消失を防ぐ
Tailwind CSSはソースファイルをスキャンして、使われているクラスだけをCSSに出力します。これがcontent scanningです。便利ですが、bg-${status}-600のようにクラス名を動的に組み立てると、Tailwindが検出できず、本番CSSに出ないことがあります。
安全な書き方は、使うクラスを完全な文字列として列挙することです。
type Status = "success" | "warning" | "danger";
const statusClasses: Record<Status, string> = {
success: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
warning: "bg-amber-50 text-amber-800 ring-amber-600/20",
danger: "bg-red-50 text-red-700 ring-red-600/20",
};
export function StatusBadge({ status, label }: { status: Status; label: string }) {
return (
<span
className={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ${statusClasses[status]}`}
>
{label}
</span>
);
}
外部パッケージやCMS由来のHTMLなど、通常のスキャン対象に入らない場所でTailwindクラスを使うなら、v4系では@sourceや@source inline()を検討します。ただし、safelistを増やしすぎるとCSSが太ります。まずはクラスを静的なmapに寄せ、それでも必要なものだけを明示します。
/* src/styles/app.css */
@import "tailwindcss";
@source "../node_modules/@acme/ui-kit";
@source inline("bg-emerald-50");
@source inline("text-emerald-700");
@source inline("bg-amber-50");
@source inline("text-amber-800");
@source inline("bg-red-50");
@source inline("text-red-700");
Claude Codeには「動的クラスを見つけたら、静的mapに置き換える。置き換えられない理由がある場合だけ@source inline()を使う」と指示すると、事故を減らせます。
見た目チェックはスクリーンショットで固定する
Tailwindの修正は、型チェックだけでは足りません。npm run buildが通っても、スマホでボタンが折り返したり、ダークモードで文字が見えなかったりします。Claude Codeに実装させたら、最後にブラウザ確認かPlaywrightのスクリーンショット比較を入れます。
Playwright Testを使っているプロジェクトなら、主要幅ごとのスクリーンショットを残すだけでもレビューが楽になります。
import { expect, test } from "@playwright/test";
const viewports = [
{ name: "mobile", size: { width: 375, height: 812 } },
{ name: "tablet", size: { width: 768, height: 1024 } },
{ name: "desktop", size: { width: 1440, height: 960 } },
];
for (const viewport of viewports) {
test(`pricing page visual check - ${viewport.name}`, async ({ page }) => {
await page.setViewportSize(viewport.size);
await page.goto("/pricing");
await expect(page.getByRole("main")).toHaveScreenshot(
`pricing-${viewport.name}.png`,
{ maxDiffPixelRatio: 0.01 },
);
});
}
Claude Codeへの検証依頼は次のようにします。
Tailwind CSSの変更後レビューをしてください。
- 変更ファイルと変更理由を一覧にする
- 375px、768px、1440pxで横スクロールがないか確認する
- ライト/ダークで本文、ボタン、フォーム、カードのコントラストを確認する
- CTA、広告枠、コードブロックが重なっていないか確認する
- Playwrightがある場合はスクリーンショットテストを追加し、なければ手動確認手順を出す
使いどころ別の進め方
| ユースケース | Claude Codeへの頼み方 | Tailwindで見るポイント |
|---|---|---|
| ランディングページ改善 | ファーストビュー、CTA、価格表を一緒に確認させる | 余白、見出しサイズ、CTAの押しやすさ |
| SaaSダッシュボード | テーブル、フィルター、サイドバーを状態別に整理させる | overflow-x-auto、sticky header、密度 |
| 問い合わせフォーム | 入力、エラー、完了、disabledを全部出させる | focus ring、ラベル、スマホ幅 |
| 記事・教材サイト | 本文、コードブロック、広告、CTAを同時に確認させる | 行間、コードの横スクロール、内部リンク |
3つ以上の画面に同じボタンやカードが出てきたら、コンポーネント抽出のタイミングです。逆に1回しか使わない装飾を全部共通化すると、変更が重くなります。
よくある落とし穴
bg-${color}-600のような動的クラスで本番CSSから消える。md:やlg:だけを見て、375px幅の確認を忘れる。dark:を背景だけに付けて、文字色、枠線、focus ringを忘れる。@applyで巨大なCSSクラスを作り、Tailwindの利点である局所性を失う。- 既存のデザイントークンを無視して、似た青、似たグレー、似た角丸を増やす。
- フォームのエラーやdisabledを確認せず、成功状態だけで公開する。
- スクリーンショット確認をせず、CTAや広告枠との重なりに気づかない。
Claude Codeには、実装後に「自分の変更を批判的にレビューして」と明示します。特にTailwindは見た目の差分が小さく見えても、CVRや読みやすさに効くため、レビュー観点を固定するのが重要です。
収益導線に組み込む
Tailwind CSSの改善は、見た目を整えるだけで終わらせるともったいないです。記事サイトなら読了後のCTA、教材販売なら価格表、SaaSなら無料トライアルのボタン、研修ページなら問い合わせフォームまで一緒に確認します。
ClaudeCodeLabでは、Claude Codeの基本コマンドを確認したい人向けに無料チートシート、プロンプトやレビュー観点を型化したい人向けに教材・テンプレート一覧、チームの導入ルールまで整えたい人向けにClaude Code研修・相談を用意しています。TailwindのUI改善も、CLAUDE.md、レビュー手順、Playwright確認をセットにすると、単発のデザイン修正ではなく継続的な改善フローになります。
まとめ
Claude CodeでTailwind CSSを使うときは、最初にデザイントークンを固め、レスポンシブとダークモードの方針を明示し、重複したボタン・フォーム・カードだけをコンポーネント化します。長すぎるclassNameはvariant mapで整理し、動的クラスは静的mapか最小限の@source inline()へ寄せます。最後にPlaywrightやブラウザでスクリーンショット確認を行えば、見た目だけでなく保守性と収益導線まで守れます。
この記事で紹介した内容を実際に試した結果、効果が大きかったのは「実装前の棚卸し」と「375px幅のスクリーンショット確認」でした。Masaの検証では、最初からトークン、CTA、フォーム状態、ダークモードをチェックリストに入れた方が、後からclassNameをほどく作業が減り、記事下の購入・相談導線も崩れにくくなりました。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Code権限セーフティラダー: 初心者がallowを広げる順番
Claude Codeの権限をread-onlyからbuild、限定編集、deploy確認まで段階的に広げる安全な運用手順。
Claude Code Small PR Proof Pack: 小さなPRをレビュー可能にする証拠セット
Claude Codeの小さなPRに、差分・検証・公開URL・CTA・rollbackを添える実務チェックリスト。
Claude Codeのコミット前レビューゲート: 差分、テスト、CTAをまとめて止める型
Claude Codeでcommit前に差分をレビューする実践手順。build、公開URL、CTA、Gumroadリンク、未翻訳本文を検知します。