AIが生成したUIを眺めていると、どうしても「なんか違う」という違和感を感じることがあります。
グラデーション、絵文字、過度な角丸、影の乱用……確かにこうした表面的な要素は目につきます。しかし、本当の問題はそこではありません。AIが生成したUIが「つくられ感」を漂わせる根本原因は、判断の不在 にあるのです。
このブログでも繰り返し語ってきた「バイブコーディング」の本質は、AIに判断を注入する という営為です。今回の記事では、その判断がUIコンポーネントの実装レベルでどのように表現されるべきかについて、具体的に解説します。
AI生成UIが「違う」と感じられる表面的な理由
まずは、目に映る「らしさ」の正体から始めましょう。
グラデーションの「安定性」
Tailwind CSSでよく見かける from-indigo-500 to-purple-600 のようなグラデーション。これは「統計的に失敗しにくい配色」です。AIモデルは大量の成功したデザインから学んでいるため、視覚的な「はずれ」が少ない配色を選びます。
ただし、そこに 意図がない のです。なぜこの色なのか。ブランドカラーはもっと濃いかもしれません。むしろモノクロメイン、アクセントだけ色を付けるべきかもしれません。
絵文字の過剰使用
🚀 🎉 ✨ 💡 🔥
AIは「親しみやすさ」を数字で最大化しようとするため、こうした絵文字を多用します。しかし、絵文字の出現箇所や数は、「対象ユーザーがどう感じるか」という人間の判断 を必要とします。
角丸とカード
rounded-2xl や rounded-full の多用も特徴的です。モダンで親しみやすい見た目——これもAIが「失敗しにくい選択」として学習したものです。
でも、本当にボタンは rounded-lg より rounded-2xl の方が良いのでしょうか。親指で押すタッチターゲットとして最適化するなら、角丸は目立たず、むしろ 44px以上の押下領域 が重要です。
本質: UIに宿る「判断」の有無
ここからが核心です。
良いUIデザインと、AI生成UIの本質的な違いは何か。それは 人間の判断 です。
CTAボタンが他の要素より目立つのは、「ユーザーに最も重要なアクションを取ってもらいたい」という判断があるから。
エラーボタンの角丸を小さくするのは、「危険な操作だから、よく見て判断させたい」という心配りがあるから。
モーダルの背景を濃くするのは、「背景のコンテンツに気を取られず、モーダル内容だけに集中させたい」という設計判断があるから。
AIはこうした 「なぜ」の積み重ね を持っていません。統計的に「良い」とされた要素の組み合わせを選ぶだけです。
判断をコードに宿す3つの方法
では、どうすればUIに判断を注入できるのでしょうか。三つの実践的なアプローチを紹介します。
1. 型で判断を表現する
まず、単純なButtonコンポーネントを考えてみます。
// AI臭い実装: 色を直接指定
interface ButtonProps {
color?: string; // 'blue' | 'red' | 'green' ...
children: React.ReactNode;
}
export function Button({ color = 'blue', children }: ButtonProps) {
return (
<button className={`bg-${color}-500 text-white px-4 py-2 rounded-lg`}>
{children}
</button>
);
}この書き方には判断がありません。呼び出し側で <Button color="red">削除</Button> とすれば、赤いボタンができますが、「なぜ赤いのか」という意図が不透明です。
では、意図を型で表現してみましょう。
// 判断を型に宿した実装
type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'ghost' | 'link';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
variant?: ButtonVariant; // 意味のある選択肢のみ
size?: ButtonSize;
isLoading?: boolean;
disabled?: boolean;
children: React.ReactNode;
}
export function Button({
variant = 'primary',
size = 'md',
isLoading = false,
disabled = false,
children
}: ButtonProps) {
const baseStyles = 'font-medium transition-all duration-200 focus:ring-2';
const variantStyles = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-300',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-400',
destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-300',
ghost: 'bg-transparent text-gray-900 hover:bg-gray-100 focus:ring-gray-300',
link: 'bg-transparent text-blue-600 underline hover:no-underline focus:ring-blue-300',
};
const sizeStyles = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base min-h-[44px]', // 高齢者対応タッチターゲット
lg: 'px-6 py-3 text-lg min-h-[48px]',
};
return (
<button
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${
isLoading ? 'opacity-70 cursor-wait pointer-events-none' : ''
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={disabled || isLoading}
>
{isLoading ? '処理中...' : children}
</button>
);
}ここで何が変わったのか:
- variant で意図を表現: 「これは主要アクション」「これは危険な操作」という意図が型として現れます
- size に44px最小化を組み込み: アクセシビリティへの判断をコードに宿します
- isLoading で状態を明示化: 「処理中」という状態に対して「待機カーソル + 不透明化 + クリック無効化」という判断を一貫させます
2. デザイントークンにブランド判断を込める
次に、Tailwindのカスタムトークンレベルで判断を示す例です。
// tailwind.config.ts - デザイントークンに判断を宿す
export default {
theme: {
colors: {
// ブランドパレット: Antigravity の世界観を表現
surface: 'oklch(0.98 0.002 0)', // 真白ではなく、わずかに温い白
'surface-alt': 'oklch(0.94 0.002 0)', // サブ背景
// セマンティックカラー: 「役割」に基づいた色
primary: 'oklch(0.55 0.2 260)', // ブランドプライマリ(青紫)
secondary: 'oklch(0.65 0.1 260)', // セカンダリ(淡い青紫)
success: 'oklch(0.58 0.15 142)', // 成功(緑)
warning: 'oklch(0.68 0.15 65)', // 警告(黄)
error: 'oklch(0.55 0.2 25)', // エラー(赤)
info: 'oklch(0.60 0.15 250)', // 情報(青)
},
spacing: {
// 判断的な余白スケール: フィボナッチ比率ベース
'xs': '0.25rem', // 4px - テキスト内余白
'sm': '0.5rem', // 8px - 密集要素間
'md': '1rem', // 16px - 標準間隔
'lg': '1.5rem', // 24px - セクション間隔
'xl': '2.5rem', // 40px - メジャーセクション
'xxl': '4rem', // 64px - ページ余白
},
fontSize: {
// タイポグラフィ: 読みやすさへの判断
'xs': ['0.75rem', { lineHeight: '1.5' }], // 12px / 1.5
'sm': ['0.875rem', { lineHeight: '1.5' }], // 14px / 1.5
'base': ['1rem', { lineHeight: '1.6' }], // 16px / 1.6
'lg': ['1.125rem', { lineHeight: '1.6' }], // 18px / 1.6
'xl': ['1.375rem', { lineHeight: '1.5' }], // 22px / 1.5
'h2': ['1.75rem', { lineHeight: '1.3' }], // 28px / 1.3
'h1': ['2.25rem', { lineHeight: '1.2' }], // 36px / 1.2
},
},
};ここで評価すべき点:
- oklch 色空間: RGB や HSL より人間の知覚に近い色定義。ブランド感度が高まります
- セマンティックな色名:
primaryerrorsuccess— 色の「役割」が明確です - line-height の意図的設定: 見出しは行間を詰める(1.2)、本文は広げる(1.6)という読みやすさへの判断
3. 条件分岐に「なぜ」を書く
最後に、コンポーネント内の条件分岐そのものが判断の表現だという例です。
// FormInput.tsx - アクセシビリティへの判断が条件分岐に宿る
interface FormInputProps {
label: string;
value: string;
onChange: (val: string) => void;
error?: string;
isLoading?: boolean;
required?: boolean;
}
export function FormInput({
label,
value,
onChange,
error,
isLoading,
required,
}: FormInputProps) {
const inputId = `input-${label.replace(/\s/g, '-')}`;
return (
<div className="flex flex-col gap-md">
<label htmlFor={inputId} className="font-medium text-gray-900">
{label}
{required && <span className="text-error ml-1">*</span>}
</label>
<input
id={inputId}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={isLoading}
// エラー状態への判断:
// 1. 視覚的に区別(赤い枠)
// 2. スクリーンリーダー利用者に伝達(aria-invalid)
// 3. aria-describedby でエラーメッセージを関連付ける
aria-invalid={!!error}
aria-describedby={error ? `${inputId}-error` : undefined}
className={`
px-3 py-2 border-2 rounded-md transition-colors
${error
? 'border-error bg-error/5 focus:ring-error/30'
: 'border-gray-300 focus:ring-primary/30'
}
${isLoading ? 'cursor-wait opacity-70' : ''}
focus:outline-none focus:ring-2
`}
/>
{/* エラー表示: role="alert" で即座に読み上げさせる */}
{error && (
<div
id={`${inputId}-error`}
role="alert"
aria-live="polite"
className="text-error text-sm font-medium"
>
{error}
</div>
)}
</div>
);
}条件分岐に込められた判断:
- aria-invalid + aria-describedby: スクリーンリーダー利用者にエラーを正確に伝えたい
- role="alert" + aria-live="polite": エラー表示を「割り込み情報」として即座に読み上げさせたい
- cursor-wait: 「処理中です、お待ちください」という心理的安心感を与えたい
なぜこうした判断が必要か
ここまでの3つのアプローチの共通点は、すべての選択に「なぜ」がある という点です。
- ボタンのバリアントが「primary」「destructive」「ghost」に限定されるのは、ユーザー心理と一貫性を考えたから
- 最小高さが44pxなのは、高齢者や指の大きな人でも操作できるようにしたから
- エラーメッセージに
aria-liveを付けるのは、全盲者にも同じリアルタイム情報を与えたいから
AIが「違う」ように見えるのは、こうした 各所に宿る意図の連鎖 が欠けているからです。逆に言えば、この「なぜ」を丁寧に組み込むだけで、生成されたコードもグッと人間らしくなるのです。
バイブコーディングでの活用
Antigravity Lab が推奨する「バイブコーディング」は、まさにこうした判断をAIに 注入する プロセスです。
# プロンプト例: デザイントークンを先に渡す
上記の tailwind.config.ts を参照してください。
以下の要件で Button コンポーネントを実装してください:
- variant: 'primary' | 'secondary' | 'destructive' | 'ghost' | 'link'
- size: 'sm' | 'md' | 'lg' (最小タッチターゲット 44px)
- isLoading 状態では cursor-wait + opacity-70 + disabled
- role, aria-* 属性で支援技術対応
- デザイントークンの色・余白・タイポグラフィのみ使用
実装後、「なぜこの条件分岐を選んだか」をコメントで説明してください。こうしてAIに「判断の根拠」を明示してから生成させると、AI出力の質は劇的に変わります。