frontend-ui-engineering について
frontend-ui-engineeringは、フロントエンドの実装や変更を行うエージェント向けのスキルです。コンポーネント構成、デザインシステム、アクセシビリティ、レスポンシブ対応、検証を扱い、汎用的なテンプレートではなく製品に合ったUIを目指します。
主な機能
- 01コンポーネント設計と状態管理の指針
- 02デザインシステムに基づく視覚設計
- 03キーボードとスクリーンリーダー対応
- 04モバイルファーストのレスポンシブレイアウト
- 05読み込み・空・エラー状態
- 06UI検証チェックリスト
活用シーン
- 01新しいページやUIコンポーネントの構築
- 02既存インターフェースの変更
- 03レスポンシブ対応やアクセシビリティの修正
- 04完了前のUI状態の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ maddec01/ mux-factory --skill frontend-ui-engineeringインストール後、Claude Code で/frontend-ui-engineeringを入力して読み込まれたか確認します。
詳細
フロントエンド UI エンジニアリング
概要
アクセシブルで、高速かつ、見た目も洗練された、本番品質のユーザーインターフェースを構築します。目指すのは、AI が生成したような見た目ではなく、一流企業のデザインに精通したエンジニアが作ったような UI です。そのためには、デザインシステムを適切に遵守し、アクセシビリティに配慮し、インタラクションパターンを慎重に設計し、ありがちな「AI らしい見た目」を避ける必要があります。
使用する場面
- 新しい UI コンポーネントやページを構築する場合
- 既存のユーザー向けインターフェースを変更する場合
- レスポンシブレイアウトを実装する場合
- インタラクティブ機能や状態管理を追加する場合
- 見た目や UX の問題を修正する場合
コンポーネントアーキテクチャ
ファイル構成
コンポーネントに関連するものはすべて、同じ場所にまとめます。
src/components/
TaskList/
TaskList.tsx # Component implementation
TaskList.test.tsx # Tests
TaskList.stories.tsx # Storybook stories (if using)
use-task-list.ts # Custom hook (if complex state)
types.ts # Component-specific types (if needed)
コンポーネントのパターン
設定よりもコンポジションを優先します。
// Good: Composable
<Card>
<CardHeader>
<CardTitle>Tasks</CardTitle>
</CardHeader>
<CardBody>
<TaskList tasks={tasks} />
</CardBody>
</Card>
// Avoid: Over-configured
<Card
title="Tasks"
headerVariant="large"
bodyPadding="md"
content={<TaskList tasks={tasks} />}
/>
コンポーネントの役割を絞ります。
// Good: Does one thing
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>
);
}
データ取得と表示を分離します。
// Container: handles data
export function TaskListContainer() {
const { tasks, isLoading, error, refetch } = 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} />;
}
// Presentation: handles rendering
export function TaskList({ tasks }: { tasks: Task[] }) {
return (
<ul role="list" className="divide-y">
{tasks.map(task => <TaskItem key={task.id} task={task} />)}
</ul>
);
}
状態管理
機能する方法のうち、最もシンプルなものを選びます。
Local state (useState) → Component-specific UI state
Lifted state → Shared between 2-3 sibling components
Context → Theme, auth, locale (read-heavy, write-rare)
URL state (searchParams) → Filters, pagination, shareable UI state
Server state (React Query, SWR) → Remote data with caching
Global store (Zustand, Redux) → Complex client state shared app-wide
3 階層を超える Props のバケツリレーは避けます。 使用しないコンポーネントを経由して Props を渡している場合は、Context を導入するか、コンポーネントツリーを再構成します。
デザインシステムの遵守
参考資料に基づく UI の品質
製品に明確なビジュアルの方向性が必要な場合は、レイアウトを選ぶ前に根拠を集めます。
- 信頼できる参考資料カタログを検索するか、製品チームから提供された参考資料を使用します。
- 関連する画面を 2〜3 個調べます。階層、情報密度、ナビゲーション、コントロール、レスポンシブ動作、インタラクション状態に関する判断を記録します。
- 実装の前に、これらの判断を短いデザイン契約にまとめます。画面の目的、主要な操作、必要な状態、レスポンシブルール、避けるべきパターンを明記します。
- 参考資料から有用な構造を、自社製品のコンポーネント、トークン、コンテンツ、ビジュアル言語で再構築します。他製品のブランディング、独自のテキストや画像、正確なレイアウトをコピーしてはいけません。
参考資料はテンプレートではなく、根拠として使用します。参考資料がない場合は、前提を記録し、製品の既存のデザインシステムと照らし合わせて結果を確認します。
AI らしい見た目を避ける
AI 生成の UI には、見分けやすいパターンがあります。以下はすべて避けてください。
| AI にありがちなもの | 問題点 | 本番品質 |
|---|---|---|
| 何でも紫やインディゴにする | モデルは視覚的に「無難」なパレットを選びがちで、アプリの見た目がどれも同じになります | プロジェクトで実際に使われているカラーパレットを使用します |
| 過剰なグラデーション | グラデーションは視覚的なノイズを増やし、ほとんどのデザインシステムと調和しません | デザインシステムに合ったフラットな色、または控えめなグラデーションを使用します |
| あらゆる要素を丸くする(rounded-2xl) | 最大限に角を丸くすると「親しみやすさ」は出ますが、実際のデザインにおける角丸の階層が無視されます | デザインシステムに沿った一貫性のある border-radius を使用します |
| ありきたりなヒーローセクション | テンプレート頼みのレイアウトで、実際のコンテンツやユーザーのニーズとのつながりがありません | コンテンツを優先したレイアウトにします |
| Lorem ipsum 風のコピー | プレースホルダーテキストでは、実際のコンテンツで明らかになるレイアウトの問題(長さ、折り返し、はみ出し)が隠れてしまいます | 現実的なプレースホルダーコンテンツを使用します |
| あらゆる場所に過剰な余白を設ける | 均一に大きな余白を設けると、視覚的な階層が損なわれ、画面スペースが無駄になります | 一貫性のあるスペーシングスケールを使用します |
| ありきたりなカードグリッド | 均一なグリッドは、情報の優先度や読み取りパターンを無視した、安易なレイアウトです | 目的に沿ったレイアウトにします |
| 影を多用したデザイン | 重ねた影は、コンテンツと競合する奥行きを生み、低性能デバイスではレンダリングを遅くします | デザインシステムで指定されていない限り、影は控えめにするか、使用しません |
スペーシングとレイアウト
一貫性のあるスペーシングスケールを使用します。値を独自に作らないでください。
/* Use the scale: 0.25rem increments (or whatever the project uses) */
/* Good */ padding: 1rem; /* 16px */
/* Good */ gap: 0.75rem; /* 12px */
/* Bad */ padding: 13px; /* Not on any scale */
/* Bad */ margin-top: 2.3rem; /* Not on any scale */
タイポグラフィ
文字の階層を守ります。
h1 → Page title (one per page)
h2 → Section title
h3 → Subsection title
body → Default text
small → Secondary/helper text
見出しレベルを飛ばさないでください。見出し以外のコンテンツに見出しスタイルを使用しないでください。
色
- セマンティックカラーのトークンを使用します。生の 16 進数値ではなく、
text-primary、bg-surface、border-defaultを使用します - 十分なコントラストを確保します(通常のテキストは 4.5:1、大きなテキストは 3:1)
- 情報を色だけで伝えないでください(アイコン、テキスト、パターンなども使用します)
アクセシビリティ(WCAG 2.1 AA)
すべてのコンポーネントは、以下の基準を満たす必要があります。
キーボードナビゲーション
// Every interactive element must be keyboard accessible
<button onClick={handleClick}>Click me</button> // ✓ Focusable by default
<div onClick={handleClick}>Click me</div> // ✗ Not focusable
<div role="button" tabIndex={0} onClick={handleClick} // ✓ But prefer <button>
onKeyDown={e => {
if (e.key === 'Enter') handleClick();
if (e.key === ' ') e.preventDefault();
}}
onKeyUp={e => {
if (e.key === ' ') handleClick();
}}>
Click me
</div>
ARIA ラベル
// Label interactive elements that lack visible text
<button aria-label="Close dialog"><XIcon /></button>
// Label form inputs
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// Or use aria-label when no visible label exists
<input aria-label="Search tasks" type="search" />
フォーカス管理
// Move focus when content changes
function Dialog({ isOpen, onClose }: DialogProps) {
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) closeRef.current?.focus();
}, [isOpen]);
// Trap focus inside dialog when open
return (
<dialog open={isOpen}>
<button ref={closeRef} onClick={onClose}>Close</button>
{/* dialog content */}
</dialog>
);
}
意味のある空状態とエラー状態
// Don't show blank screens
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: mobile-first responsive
<div className="
grid grid-cols-1 /* Mobile: single column */
sm:grid-cols-2 /* Small: 2 columns */
lg:grid-cols-3 /* Large: 3 columns */
gap-4
">
次のブレークポイントでテストしてください:320px、768px、1024px、1440px。
読み込みとトランジション
// Skeleton loading (not spinners for content)
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>
);
}
// Optimistic updates for perceived speed
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 にアクセシビリティ警告がない