前端 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>
);
}
狀態管理
選擇最簡單且能解決問題的方法:
Local state (useState) → 元件特定的 UI 狀態
Lifted state → 在 2-3 個兄弟元件間共享
Context → 主題、認證、語系(讀取頻繁、寫入稀少)
URL state (searchParams) → 篩選、分頁、可分享的 UI 狀態
Server state (React Query, SWR) → 具有快取的遠端資料
Global store (Zustand, Redux) → 應用程式範圍內共享的複雜客戶端狀態
避免 prop 傳遞超過 3 層。 如果你正在將 props 傳遞給未使用它們的元件,請引入 context 或重構元件樹。
設計系統遵循
以參考資料為導向的 UI 品質
當產品需要獨特的視覺方向時,在選擇版面配置前先收集證據:
- 搜尋值得信賴的參考目錄,或使用產品團隊提供的參考資料。
- 研究兩個或三個相關的畫面。記錄關於層級、密度、導航、控制項、響應式行為和互動狀態的決策。
- 在實作之前,將這些決策轉換為簡短的「設計合約」。定義畫面的功能、主要操作、必要狀態、響應式規則以及要拒絕的模式。
- 使用產品自己的元件、代碼、內容和視覺語言重建有用的結構。切勿複製其他產品的品牌、專有文字、圖像或精確的版面配置。
將參考資料視為證據,而非範本。如果無法取得參考資料,請記錄假設,並根據產品現有的設計系統驗證結果。
避免 AI 美學
AI 生成的 UI 具有可辨識的模式。請避免所有這些模式:
| AI 預設值 | 為什麼這是個問題 | 生產品質的做法 |
|---|---|---|
| 到處都是紫色/靛藍色 | 模型會預設選擇視覺上「安全」的色板,導致每個應用程式看起來都一模一樣 | 使用專案實際的色板 |
| 過度的漸層 | 漸層會增加視覺噪音,並與大多數設計系統衝突 | 使用符合設計系統的純色或精緻漸層 |
| 到處都是圓角(rounded-2xl) | 最大圓角代表「友善」,但忽略了真實設計中圓角半徑的層級 | 使用設計系統中一致的圓角半徑 |
| 通用的英雄區域 | 範本驅動的版面配置,與實際內容或使用者需求無關 | 以內容為優先的版面配置 |
| Lorem ipsum 風格的文字 | 佔位文字會隱藏真實內容所揭露的版面問題(長度、換行、溢出) | 使用真實的佔位內容 |
| 各處都過大的內距 | 均等的慷慨內距會破壞視覺層級並浪費螢幕空間 | 使用一致的間距尺度 |
| 標準的卡片網格 | 統一的網格是一種版面配置捷徑,忽略了資訊優先級和掃描模式 | 以目的為導向的版面配置 |
| 陰影過重的設計 | 多層陰影會增加深度,與內容競爭注意力,並在低階裝置上減慢渲染速度 | 使用微妙的陰影或無陰影,除非設計系統有指定 |
間距與版面配置
使用一致的間距尺度。不要發明數值:
/* 使用尺度:0.25rem 遞增(或專案使用的任何尺度) */
/* 良好 */ padding: 1rem; /* 16px */
/* 良好 */ gap: 0.75rem; /* 12px */
/* 不良 */ padding: 13px; /* 不在任何尺度上 */
/* 不良 */ margin-top: 2.3rem; /* 不在任何尺度上 */
字體排印
尊重標題層級結構:
h1 → 頁面標題(每頁一個)
h2 → 章節標題
h3 → 小節標題
body → 預設文字
small → 次要/輔助文字
不要跳過標題層級。不要將標題樣式用於非標題內容。
色彩
- 使用語意化色彩代碼:
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。
載入與轉場
// 骨架載入(內容載入時不要使用旋轉圖示)
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。
常見的合理化解釋
| 合理化 | 現實 |
|---|---|
| 「無障礙性是非必要的」 | 在許多司法管轄區這是法律要求,也是工程品質標準。 |
| 「我們之後再處理響應式」 | 事後補強響應式設計比從頭開始建構困難三倍。 |
| 「設計還沒定稿,所以我先跳過樣式」 | 使用設計系統的預設值。未經樣式處理的 UI 會為審查者留下不良的第一印象。 |
| 「這只是個原型」 | 原型最終會變成正式程式碼。從一開始就把基礎打好。 |
| 「AI 美學目前還可以」 | 它傳達了低品質的訊號。請從一開始就使用專案實際的設計系統。 |
警示訊號
- 超過 200 行的元件(請拆分它們)
- 行內樣式或任意的像素值
- 缺少錯誤狀態、載入狀態或空狀態
- 未測試鍵盤導航
- 顏色作為狀態的唯一指標(紅色/綠色沒有文字或圖示)
- 通用的「AI 外觀」(紫色漸層、過大的卡片、標準的網格)
驗證
建置 UI 後:
- 元件渲染時沒有主控台錯誤
- 所有可互動元素皆可透過鍵盤操作(使用 Tab 鍵瀏覽頁面)
- 螢幕閱讀器能夠傳達頁面的內容和結構
- 響應式:在 320px、768px、1024px、1440px 下正常運作
- 已處理載入、空白、錯誤、成功和權限狀態(如適用)
- 遵循專案的設計系統(間距、色彩、字體排印)
- 渲染結果通過最終的 UI 專屬完成審查
- 開發者工具或 axe-core 中沒有無障礙性警告