frontend-ui-engineering
プロダクション品質のUIを構築します。ユーザーが目にするインターフェースの構築や修正を行う際に使用してください。コンポーネントの作成、レイアウトの実装、ステート管理など、AIが生成したものではなく、プロダクション品質の外観と操作感が必要な場合に活用できます。
description の原文を見る
Builds production-quality UIs. Use when building or modifying user-facing interfaces. Use when creating components, implementing layouts, managing state, or when the output needs to look and feel production-quality rather than AI-generated.
SKILL.md 本文
フロントエンド UI エンジニアリング
概要
アクセシブルで高性能かつ視覚的に洗練された、本番品質のユーザーインターフェースを構築します。目標は、一流企業のデザイン志向のエンジニアが構築したように見える 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 つのシブリングコンポーネント間で共有
Context → テーマ、認証、ロケール(読み取り頻度が高く、書き込み頻度が低い)
URL 状態 (searchParams) → フィルター、ページネーション、共有可能な UI 状態
サーバー状態 (React Query, SWR) → キャッシング付きのリモートデータ
グローバルストア (Zustand, Redux) → アプリ全体で共有される複雑なクライアント状態
プロップドリリングを 3 レベルより深くしないようにします。 プロップを使用しないコンポーネント経由で渡している場合は、Context を導入するか、コンポーネントツリーを再構成します。
デザインシステムへの準拠
AI 美学の回避
AI が生成した UI には認識可能なパターンがあります。すべてを回避します:
| AI のデフォルト | 問題である理由 | 本番品質 |
|---|---|---|
| 紫/インディゴばかり | モデルは視覚的に「安全な」パレットをデフォルトにするため、すべてのアプリが同じように見えます | プロジェクトの実際のカラーパレットを使用します |
| 過度なグラデーション | グラデーションは視覚的なノイズを加え、ほとんどのデザインシステムと競合します | デザインシステムに合わせたフラットまたは微妙なグラデーション |
| すべてを角丸く(rounded-2xl) | 最大の角丸は「親しみやすさ」を示していますが、実際のデザイン内の border-radius の階層を無視します | デザインシステムの一貫した border-radius |
| 一般的なヒーローセクション | テンプレート駆動のレイアウトで、実際のコンテンツまたはユーザーニーズとの関連がありません | コンテンツファースト レイアウト |
| Lorem ipsum スタイルのコピー | プレースホルダーテキストはレイアウト問題(長さ、折り返し、オーバーフロー)を隠します | リアルなプレースホルダーコンテンツ |
| どこでも過度なパディング | 等しい寛大なパディングは視覚的階層を破壊し、画面スペースを浪費します | 一貫したスペーシングスケール |
| 一般的なカードグリッド | 均一なグリッドはレイアウトショートカットであり、情報の優先順位とスキャニングパターンを無視します | 目的駆動のレイアウト |
| シャドウが重いデザイン | レイヤー化されたシャドウはコンテンツと競合する深さを加え、低機能デバイスでレンダリングを低下させます | デザインシステムで指定されている場合のみ、微妙なシャドウまたはシャドウなし |
スペーシングとレイアウト
一貫したスペーシングスケールを使用します。値を発明しないでください:
/* スケールを使用します: 0.25rem 増分(またはプロジェクトが使用するもの) */
/* 良い */ padding: 1rem; /* 16px */
/* 良い */ gap: 0.75rem; /* 12px */
/* 悪い */ padding: 13px; /* スケール上にない */
/* 悪い */ margin-top: 2.3rem; /* スケール上にない */
タイポグラフィ
タイプ階層を尊重します:
h1 → ページタイトル(ページごとに 1 つ)
h2 → セクションタイトル
h3 → サブセクションタイトル
body → デフォルトテキスト
small → セカンダリ/ヘルパーテキスト
見出しレベルをスキップしないでください。見出しでないコンテンツに見出しスタイルを使用しないでください。
色
- セマンティックカラートークンを使用します:
text-primary、bg-surface、border-default— raw hex 値ではなく - 十分なコントラストを確保します(通常のテキストは 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 /* モバイル: 単一列 */
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 を構築した後:
- コンポーネントがコンソールエラーなしでレンダリングされます
- すべてのインタラクティブ要素はキーボードでアクセス可能です(ページをタブで移動します)
- スクリーンリーダーがページのコンテンツと構造を伝えることができます
- レスポンシブ: 320px、768px、1024px、1440px で動作します
- ローディング、エラー、空の状態がすべて処理されます
- プロジェクトのデザインシステムに従っています(スペーシング、色、タイポグラフィ)
- 開発者ツールまたは axe-core のアクセシビリティ警告がありません
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- addyosmani
- ライセンス
- MIT
- 最終更新
- 2026/5/10
Source: https://github.com/addyosmani/agent-skills / ライセンス: MIT