フロントエンドUIエンジニアリング は何をしますか?
概要
アクセシブルで、パフォーマンスが高く、視覚的に洗練されたプロダクション品質のユーザーインターフェースを構築します。目標は、AIが生成したように見えるのではなく、トップ企業のデザインを意識したエンジニアが構築したようなUIです。これは、実際のデザインシステムへの準拠、適切なアクセシビリティ、思慮深いインタラクションパターン、そして一般的な「AI美学」の排除を意味します。
使用すべきタイミング
- 新しいUIコンポーネントやページの構築
- 既存のユーザー向けインターフェースの修正
- レスポンシブレイアウトの実装
- インタラクティビティや状態管理の追加
- ビジュアルやUXの問題の修正
コンポーネントアーキテクチャ
ファイル構成
コンポーネントに関連するすべてを同じ場所に配置します:
src/components/
TaskList/
TaskList.tsx # コンポーネントの実装
TaskList.test.tsx # テスト
TaskList.stories.tsx # Storybookストーリー(使用する場合)
use-task-list.ts # カスタムフック(複雑な状態の場合)
types.ts # コンポーネント固有の型(必要な場合)
コンポーネントパターン
構成よりもコンポジションを優先:
// 良い: コンポーザブル
<Card>
<CardHeader>
<CardTitle>Tasks</CardTitle>
</CardHeader>
<CardBody>
<TaskList tasks={tasks} />
</CardBody>
</Card>
// 避けるべき: 過度に設定可能
<Card
title="Tasks"
headerVariant="large"
bodyPadding="md"
content={<TaskList tasks={tasks} />}
/>
コンポーネントは焦点を絞る:
// 良い: 1つのことを行う
export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
return (
<li className="flex items-center gap-3 p-3">
<Checkbox checked={task.done} onChange={() => onToggle(task.id)} />
<span className={task.done ? 'line-through text-muted' : ''}>{task.title}</span>
<Button variant="ghost" size="sm" onClick={() => onDelete(task.id)}>
<TrashIcon />
</Button>
</li>
);
}
データ取得とプレゼンテーションを分離:
// コンテナ: データを処理
export function TaskListContainer() {
const { tasks, isLoading, error } = useTasks();
if (isLoading) return <TaskListSkeleton />;
if (error) return <ErrorState message="Failed to load tasks" retry={refetch} />;
if (tasks.length === 0) return <EmptyState message="No tasks yet" />;
return <TaskList tasks={tasks} />;
}
// プレゼンテーション: レンダリングを処理
export function TaskList({ tasks }: { tasks: Task[] }) {
return (
<ul role="list" className="divide-y">
{tasks.map(task => <TaskItem key={task.id} task={task} />)}
</ul>
);
}
状態管理
機能する最もシンプルなアプローチを選択:
ローカル状態 (useState) → コンポーネント固有のUI状態
リフトアップされた状態 → 2〜3個の兄弟コンポーネント間で共有
コンテキスト → テーマ、認証、ロケール(読み取りが多く、書き込みが少ない)
URL状態 (searchParams) → フィルター、ページネーション、共有可能なUI状態
サーバー状態 (React Query, SWR) → キャッシュによるリモートデータ
グローバルストア (Zustand, Redux) → アプリ全体で共有される複雑なクライアント状態
プロップドリリングを3階層より深くしないでください。 使用しないコンポーネントを介してpropsを渡している場合は、コンテキストを導入するか、コンポーネントツリーを再構築してください。
デザインシステムへの準拠
リファレンス主導のUI品質
プロダクトに明確なビジュアルの方向性が必要な場合、レイアウトを選択する前にエビデンスを収集します:
- 信頼できるリファレンスカタログを検索するか、プロダクトチームから提供されたリファレンスを使用します。
- 関連する2〜3の画面を調査します。階層、密度、ナビゲーション、コントロール、レスポンシブ動作、インタラクション状態に関する決定を記録します。
- 実装前に、それらの決定を短いデザイン契約に変換します。画面の役割、主要なアクション、必要な状態、レスポンシブルール、拒否するパターンを指定します。
- 有用な構造を、プロダクト独自のコンポーネント、トークン、コンテンツ、ビジュアル言語で再構築します。他のプロダクトのブランディング、独自のテキスト、画像、正確なレイアウトを決してコピーしないでください。
リファレンスはテンプレートではなく、エビデンスとして使用します。リファレンスが利用できない場合は、前提条件を文書化し、結果をプロダクトの既存のデザインシステムに対して検証します。
AI美学を避ける
AI生成のUIには認識可能なパターンがあります。これらすべてを避けてください:
| AIのデフォルト | 問題点 | プロダクション品質 |
|---|---|---|
| すべてが紫/藍色 | モデルは視覚的に「安全な」パレットをデフォルトとし、すべてのアプリが同じように見える | プロジェクトの実際のカラーパレットを使用する |
| 過剰なグラデーション | グラデーションは視覚的なノイズを追加し、ほとんどのデザインシステムと衝突する | デザインシステムに一致するフラットまたは微妙なグラデーション |
| すべてが丸い (rounded-2xl) | 最大の丸みは「親しみやすさ」を示すが、実際のデザインにおける角丸の階層を無視する | デザインシステムからの一貫したborder-radius |
| 汎用的なヒーローセクション | テンプレート駆動のレイアウトで、実際のコンテンツやユーザーニーズとの関連性がない | コンテンツファーストのレイアウト |
| Lorem ipsum風のコピー | プレースホルダーテキストは、実際のコンテンツが明らかにするレイアウトの問題(長さ、折り返し、オーバーフロー)を隠す | 現実的なプレースホルダーコンテンツ |
| どこでも過剰なパディング | 均等な大きなパディングは視覚的階層を破壊し、画面スペースを無駄にする | 一貫したスペーシングスケール |
| ストックカードグリッド | 均一なグリッドは、情報の優先順位とスキャンパターンを無視するレイアウトのショートカット | 目的主導のレイアウト |
| 影だらけのデザイン | レイヤー化された影はコンテンツと競合する奥行きを追加し、ローエンドデバイスでのレンダリングを遅くする | デザインシステムで指定されていない限り、影は微妙か無し |
スペーシングとレイアウト
一貫したスペーシングスケールを使用します。値を独自に発明しないでください:
/* スケールを使用: 0.25rem刻み(またはプロジェクトが使用するもの) */
/* 良い */ padding: 1rem; /* 16px */
/* 良い */ gap: 0.75rem; /* 12px */
/* 悪い */ padding: 13px; /* どのスケールにもない */
/* 悪い */ margin-top: 2.3rem; /* どのスケールにもない */
タイポグラフィ
タイプ階層を尊重:
h1 → ページタイトル(1ページに1つ)
h2 → セクションタイトル
h3 → サブセクションタイトル
body → デフォルトテキスト
small → セカンダリ/ヘルパーテキスト
見出しレベルをスキップしないでください。見出し以外のコンテンツに見出しスタイルを使用しないでください。
カラー
- セマンティックカラートークンを使用:
text-primary、bg-surface、border-default— 生の16進数値は使用しない - 十分なコントラストを確保する(通常テキストは4.5:1、大規模テキストは3:1)
- 情報を伝えるために色だけに依存しない(アイコン、テキスト、またはパターンも使用する)
アクセシビリティ (WCAG 2.1 AA)
すべてのコンポーネントはこれらの基準を満たす必要があります:
キーボードナビゲーション
// すべてのインタラクティブ要素はキーボードでアクセス可能である必要があります
<button onClick={handleClick}>Click me</button> // ✓ デフォルトでフォーカス可能
<div onClick={handleClick}>Click me</div> // ✗ フォーカス不可
<div role="button" tabIndex={0} onClick={handleClick} // ✓ ただし<button>を推奨
onKeyDown={e => {
if (e.key === 'Enter') handleClick();
if (e.key === ' ') e.preventDefault();
}}
onKeyUp={e => {
if (e.key === ' ') handleClick();
}}>
Click me
</div>
ARIAラベル
// 可視テキストがないインタラクティブ要素にラベルを付ける
<button aria-label="Close dialog"><XIcon /></button>
// フォーム入力にラベルを付ける
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// または、可視ラベルがない場合はaria-labelを使用
<input aria-label="Search tasks" type="search" />
フォーカス管理
// コンテンツが変更されたときにフォーカスを移動する
function Dialog({ isOpen, onClose }: DialogProps) {
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) closeRef.current?.focus();
}, [isOpen]);
// 開いている間はダイアログ内にフォーカスをトラップ
return (
<dialog open={isOpen}>
<button ref={closeRef} onClick={onClose}>Close</button>
{/* ダイアログコンテンツ */}
</dialog>
);
}
意味のある空状態とエラー状態
// 空白の画面を表示しない
function TaskList({ tasks }: { tasks: Task[] }) {
if (tasks.length === 0) {
return (
<div role="status" className="text-center py-12">
<TasksEmptyIcon className="mx-auto h-12 w-12 text-muted" />
<h3 className="mt-2 text-sm font-medium">No tasks</h3>
<p className="mt-1 text-sm text-muted">Get started by creating a new task.</p>
<Button className="mt-4" onClick={onCreateTask}>Create Task</Button>
</div>
);
}
return <ul role="list">...</ul>;
}
レスポンシブデザイン
モバイルファーストでデザインし、その後拡張:
// Tailwind: モバイルファーストのレスポンシブ
<div className="
grid grid-cols-1 /* モバイル: 1カラム */
sm:grid-cols-2 /* 小: 2カラム */
lg:grid-cols-3 /* 大: 3カラム */
gap-4
">
これらのブレークポイントでテスト: 320px、768px、1024px、1440px
ローディングとトランジション
// スケルトンローディング(コンテンツにはスピナーではなく)
function TaskListSkeleton() {
return (
<div className="space-y-3" aria-busy="true" aria-label="Loading tasks">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-12 bg-muted animate-pulse rounded" />
))}
</div>
);
}
// 知覚速度のための楽観的更新
function useToggleTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: toggleTask,
onMutate: async (taskId) => {
await queryClient.cancelQueries({ queryKey: ['tasks'] });
const previous = queryClient.getQueryData(['tasks']);
queryClient.setQueryData(['tasks'], (old: Task[]) =>
old.map(t => t.id === taskId ? { ...t, done: !t.done } : t)
);
return { previous };
},
onError: (_err, _taskId, context) => {
queryClient.setQueryData(['tasks'], context?.previous);
},
});
}
関連項目
詳細なアクセシビリティ要件とテストツールについては、../../references/accessibility-checklist.md を参照してください。
一般的な言い訳
| 言い訳 | 現実 |
|---|---|
| 「アクセシビリティはあれば良いもの」 | 多くの法域で法的要件であり、エンジニアリング品質基準です。 |
| 「後でレスポンシブにする」 | レスポンシブデザインの後付けは、最初から構築するよりも3倍難しい。 |
| 「デザインが確定していないので、スタイリングをスキップする」 | デザインシステムのデフォルトを使用してください。スタイルなしのUIは、レビューアに悪い第一印象を与えます。 |
| 「これは単なるプロトタイプです」 | プロトタイプはプロダクションコードになります。基盤を正しく構築しましょう。 |
| 「AI美学は今のところは大丈夫」 | 低品質を示します。最初からプロジェクトの実際のデザインシステムを使用してください。 |
レッドフラグ
- 200行を超えるコンポーネント(分割する)
- インラインスタイルまたは任意のピクセル値
- エラー状態、ローディング状態、または空状態の欠落
- キーボードナビゲーションテストの欠如
- 状態の唯一のインジケーターとしての色(テキストやアイコンのない赤/緑)
- 一般的な「AIルック」(紫のグラデーション、特大カード、ストックレイアウト)
検証
UI構築後:
- コンポーネントがコンソールエラーなしでレンダリングされる
- すべてのインタラクティブ要素がキーボードでアクセス可能である(Tabキーでページを移動)
- スクリーンリーダーがページのコンテンツと構造を伝えられる
- レスポンシブ: 320px、768px、1024px、1440pxで動作
- 該当する場合、ローディング、空、エラー、成功、権限の状態が処理されている
- プロジェクトのデザインシステムに従っている(スペーシング、色、タイポグラフィ)
- レンダリング結果が最終的なUI固有の仕上げゲートレビューに合格する
- 開発ツールまたはaxe-coreにアクセシビリティ警告がない