SKILL.md
readonlyread-only
name
frontend-ui-engineering
description
建立符合生產標準、無障礙且響應式的使用者介面。在建立或修改介面與頁面、建立元件、實作版面配置、滿足 WCAG 無障礙需求、管理狀態,或當輸出需要看起來像生產級品質而非 AI 生成時使用。
前端 UI 工程
概述
建立符合生產標準的使用者介面,具備無障礙、高效能且視覺精緻。目標是讓 UI 看起來像是由頂尖公司中具備設計意識的工程師所打造,而非由 AI 生成。這意味著要遵循真正的設計系統、正確的無障礙設計、深思熟慮的互動模式,並且避免通用的「AI 美學」。
使用時機
- 建立新的 UI 元件或頁面
- 修改現有的使用者介面
- 實作響應式版面配置
- 加入互動性或狀態管理
- 修正視覺或 UX 問題
元件架構
檔案結構
將與元件相關的所有內容放在一起:
src/components/
TaskList/
TaskList.tsx # 元件實作
TaskList.test.tsx # 測試
TaskList.stories.tsx # Storybook 故事(如有使用)
use-task-list.ts # 自訂 hook(若狀態複雜)
types.ts # 元件專用型別(如有需要)
元件模式
組合優於設定:
// 好:可組合
<Card>
<CardHeader>
<CardTitle>任務</CardTitle>
</CardHeader>
<CardBody>
<TaskList tasks={tasks} />
</CardBody>
</Card>
// 避免:過度設定
<Card
title="任務"
headerVariant="large"
bodyPadding="md"
content={<TaskList tasks={tasks} />}
/>
保持元件專注:
// 好:只做一件事
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="載入任務失敗" retry={refetch} />;
if (tasks.length === 0) return <EmptyState message="尚無任務" />;
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) → 應用程式範圍的複雜客戶端狀態
避免 props 鑽孔超過 3 層。 如果你正在將 props 傳遞給未使用它們的元件,請引入 context 或重構元件樹。
設計系統遵循
避免 AI 美學
AI 生成的 UI 有可識別的模式。避免所有這些:
| AI 預設 | 問題所在 | 生產級品質 |
|---|---|---|
| 到處都是紫色/靛藍色 | 模型預設使用視覺上「安全」的色板,導致每個應用程式看起來都一樣 | 使用專案實際的色板 |
| 過多的漸層 | 漸層增加視覺雜訊,並與大多數設計系統衝突 | 平面或符合設計系統的微妙漸層 |
| 到處都是圓角 (rounded-2xl) | 最大圓角表示「友善」,但忽略了真實設計中圓角半徑的層級 | 來自設計系統的一致 border-radius |
| 通用的英雄區塊 | 模板驅動的佈局,與實際內容或使用者需求無關 | 內容優先的佈局 |
| Lorem ipsum 風格的文字 | 佔位文字隱藏了真實內容會揭示的佈局問題(長度、換行、溢出) | 逼真的佔位內容 |
| 到處都是過大的 padding | 均勻的寬鬆 padding 破壞了視覺層級並浪費螢幕空間 | 一致的間距尺度 |
| 標準卡片網格 | 均勻網格是一種佈局捷徑,忽略了資訊優先級和掃描模式 | 目的驅動的佈局 |
| 大量陰影的設計 | 多層陰影增加了與內容競爭的深度,並在低階裝置上減慢渲染 | 除非設計系統指定,否則使用微妙或無陰影 |
間距與佈局
使用一致的間距尺度。不要發明數值:
/* 使用尺度:0.25rem 增量(或專案使用的任何尺度) */
/* 好 */ padding: 1rem; /* 16px */
/* 好 */ gap: 0.75rem; /* 12px */
/* 壞 */ padding: 13px; /* 不在任何尺度上 */
/* 壞 */ margin-top: 2.3rem; /* 不在任何尺度上 */
字體排版
尊重字體層級:
h1 → 頁面標題(每頁一個)
h2 → 章節標題
h3 → 子章節標題
body → 預設文字
small → 次要/輔助文字
不要跳過標題層級。不要對非標題內容使用標題樣式。
色彩
- 使用語意色彩 token:
text-primary、bg-surface、border-default— 不要使用原始十六進位值 - 確保足夠的對比度(一般文字 4.5:1,大字型 3:1)
- 不要僅依賴色彩來傳達資訊(也要使用圖示、文字或模式)
無障礙 (WCAG 2.1 AA)
每個元件都必須符合這些標準:
鍵盤導航
// 每個互動元素都必須可透過鍵盤操作
<button onClick={handleClick}>點我</button> // ✓ 預設可聚焦
<div onClick={handleClick}>點我</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();
}}>
點我
</div>
ARIA 標籤
// 為缺少可見文字的互動元素加上標籤
<button aria-label="關閉對話框"><XIcon /></button>
// 為表單輸入加上標籤
<label htmlFor="email">電子郵件</label>
<input id="email" type="email" />
// 或當沒有可見標籤時使用 aria-label
<input aria-label="搜尋任務" 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}>關閉</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">尚無任務</h3>
<p className="mt-1 text-sm text-muted">立即建立新任務來開始吧。</p>
<Button className="mt-4" onClick={onCreateTask}>建立任務</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。
載入與轉場
// 骨架載入(內容不要用 spinner)
function TaskListSkeleton() {
return (
<div className="space-y-3" aria-busy="true" aria-label="載入任務中">
{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 下正常運作
- [ ] 已處理載入、錯誤和空狀態
- [ ] 遵循專案的設計系統(間距、色彩、字體排版)
- [ ] 開發工具或 axe-core 中無無障礙警告






