AIにUIを生成させたら、なんか違和感があった。
最初は「グラデーションがダメなんだろうな」と思ってた。あの from-indigo-500 to-purple-600 みたいな奴。削除したら少しマシになった。次は絵文字が気になった。ボタンに🚀とか✨とかついてた。消した。
でも何かまだ「臭い」。
グラデーションでも絵文字でもない、もっと根本的な何かがある。それが「判断の不在」だった。
表層対策、やりがち
AI生成UIがAIっぽく見える理由として、よく挙げられるのはこのあたり。
-
グラデーション:
from-indigo-500 to-purple-600の組み合わせ - 絵文字の多用: ボタンやラベルに🚀✨💡が散りばめられている
-
過剰な角丸:
rounded-2xlやrounded-fullがどこにでも - カード多用: 何でもとりあえず白カードに入れる
-
影の多用:
shadow-lgを付けとけば良いという思想
これ、全部「統計的に安全なパターン」。学習データに大量に含まれてたから生成される。消せば臭いは薄れる。
でも消してもまだ違和感があるなら、深いところに原因がある。
本当の原因:「なぜこのUIなのか」を誰も決めていない
こういうコンポーネントを見てほしい。
// AI が生成したボタン(よくある)
export function Button({ label }: { label: string }) {
return (
<button className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg">
{label}
</button>
)
}
何が問題かというと、このボタンには「なぜ blue-500 なのか」「なぜ rounded-lg なのか」の判断が一切ない。
汎用的に「なんとなく使えそう」なパターンを出してきただけ。それがAI臭の正体。
人間がデザインしたUIには、こういう判断が滲み出ている。
- 「このページの主役はCTAだから、ボタンは他の要素より1段階濃くする」
- 「エラーボタンはアクション前に一瞬躊躇させたいから、あえて角丸を小さくする」
- 「このフォームは高齢ユーザーも使うから、クリック領域を44px以上に保つ」
これが「判断」。実装に判断が宿ると、AIが生成したのと同じ見た目でも、なぜか違う感じになる。
実装に「判断」を宿らせる3つの方法
1. 型で判断を表現する
variant の型を「視覚的な見た目」ではなく「意味・文脈」で設計する。
// ❌ 見た目で分類(判断がない)
type ButtonVariant = "blue" | "red" | "gray"
// ✅ 文脈で分類(判断がある)
type ButtonVariant = "primary" | "destructive" | "ghost"
"blue" は「青くしろ」という命令。"primary" は「このページで一番重要なアクションだ」という判断。
この差は読む人には分からないかもしれないけど、コンポーネントを使う人間(自分含む)に伝わる。「このボタンはなぜここにあるのか」が型から分かる。
2. Tailwind トークンにブランド判断を込める
Tailwind v4 の CSS変数機能を使うと、色の命名に判断を入れられる。
/* tailwind.config のかわりに CSS で定義(v4 スタイル) */
@theme {
--color-action: oklch(55% 0.2 250); /* CTAボタン専用。他では使わない */
--color-action-hover: oklch(50% 0.2 250);
--color-surface: oklch(98% 0 0); /* カード背景。白じゃなくてわずかに暖色 */
--color-muted: oklch(60% 0 0); /* 補足テキスト。本文より2段下げる */
}
--color-action は「アクションボタンに使う色」という判断を込めたトークン。blue-500 みたいな汎用名じゃなく、「なぜその色か」が名前から読める。
使う側もこう書ける。
// "なんとなく青" じゃなく "これはアクションボタンだ"
<button className="bg-action hover:bg-action-hover text-white">
送信
</button>
3. 条件分岐に「なぜ」を書く
ステートによってUIが変わるとき、変わる理由をコードに残す。
// ❌ 見た目だけが変わる(判断が隠れてる)
<button className={isLoading ? "opacity-50 cursor-not-allowed" : "opacity-100"}>
{isLoading ? "..." : "送信"}
</button>
// ✅ 意味が変わる(判断が出てる)
<button
disabled={isLoading}
aria-busy={isLoading}
className={isLoading
? "bg-muted cursor-wait" // 処理中:待機状態を示す
: "bg-action hover:bg-action-hover" // 待機中:アクション可能を示す
}
>
{isLoading ? "送信中..." : "送信する"}
</button>
cursor-wait は「今は待ってほしい」という判断。aria-busy は「スクリーンリーダーにも伝えたい」という判断。こういう1つ1つの判断の積み重ねが、AIっぽくないUIを作る。
Before / After で見ると分かりやすい
// Before(AI生成そのまま)
export function StatusBadge({ status }: { status: string }) {
return (
<span className="bg-green-100 text-green-800 px-2 py-1 rounded-full text-sm">
{status}
</span>
)
}
// After(判断を入れた)
type Status = "active" | "pending" | "error"
const statusConfig: Record<Status, { label: string; className: string }> = {
active: { label: "稼働中", className: "bg-emerald-50 text-emerald-700 border border-emerald-200" },
pending: { label: "処理中", className: "bg-amber-50 text-amber-700 border border-amber-200" },
error: { label: "エラー", className: "bg-red-50 text-red-700 border border-red-200" },
}
export function StatusBadge({ status }: { status: Status }) {
const { label, className } = statusConfig[status]
return (
<span className={`px-2 py-1 rounded text-sm font-medium ${className}`}>
{label}
</span>
)
}
Before は「string を受け取ってとりあえず緑で出す」。After は「3種類のステートそれぞれに意味と見た目の判断がある」。
見た目の差は小さいかもしれないけど、コードを読んだとき全然違う。After には「なぜこのデザインか」が詰まってる。
まとめ
グラデーションや絵文字を消してもAI臭が残るなら、それは「判断の不在」が原因かもしれない。
AIは統計的に安全なパターンを出してくる。それ自体は悪くない。問題は、そのパターンをそのまま使うこと。「なぜこのUIか」という判断を自分で一度でも通すと、同じコードでも全然違う質感になる。
型の命名、Tailwindトークン、条件分岐のクラス名。どれも小さいけど、ここに判断を入れるかどうかがAI生成UIと人間のUIの分かれ目だと思ってる。
