前端無障礙
無障礙是每次 UI 變更的一部分——從設計、建置到測試都包含在內。一項 UI 任務只有在下列條件下才算完成:
- 僅使用鍵盤即可操作
- 每個控制項都能正確宣告名稱、角色與狀態
- 自動化檢查通過
- 測試以角色 (role) 進行查詢
目標:WCAG 2.2 AA 等級——在歐盟,歐洲無障礙法案(各國法律如 BFSG、EN 301 549)對許多產品而言是強制性的。一項缺失會引用 WCAG A/AA 標準或一個輔助技術使用者無法完成的具體任務。AAA 標準與慣例(例如一個 <h1>、44 px 觸控目標)屬於建議,永遠不會成為阻擋因素。
| 任務 | 參考文件 |
|---|---|
| 選擇元素、地標 (landmarks)、標題、ARIA、可存取名稱、停用狀態 | references/semantics-and-aria.md |
| 焦點環、定位順序、跳過鏈結、對話框、路由變更、Widget 鍵盤對應 | references/focus-and-keyboard.md |
| 標籤、自動完成、輸入類型、驗證錯誤、提交行為 | references/forms.md |
| 宣告變更、動態區域 (live regions)、提示訊息、替代文字、SVG、多媒體 | references/screen-readers.md |
| 對比度、色彩、觸控目標尺寸、動畫、縮放、回流 (reflow)、內容處理 | references/visual-and-motion.md |
| 自動化與手動測試、CI | references/testing.md |
| 現有 UI 的稽核/審查、報告格式 | references/audit.md |
| 查詢 WCAG 2.2 標準 | references/wcag.md |
核心規則——始終遵守
原生優先。 動作使用 <button>,導航使用 <a href> / 路由器的 <Link>,表單使用 <label>、<fieldset>,表格使用 <table>,對話框使用 <dialog>,地標 (landmarks) 使用對應標籤。絕不使用 <div onClick>。沒有 ARIA 勝過錯誤的 ARIA;一個角色 (role) 承諾了完整的鍵盤操作模型。複雜的 Widget(如 combobox、選單、日期選擇器)應使用專案中可存取的無頭 (headless) 函式庫,而非自行手刻。
每個互動元素都必須有可存取名稱 [4.1.2]。純圖示按鈕使用 aria-label,圖示本身使用 aria-hidden="true"。可見的標籤文字必須包含在可存取名稱中 [2.5.3]。連結必須清楚表明去向——不要使用「點擊這裡」 [2.4.4]。
鍵盤 [2.1.1, 2.1.2]。任何可以用指標操作的功能都必須能用鍵盤操作;不能有鍵盤陷阱。定位順序 = 視覺順序;絕不使用正數的 tabIndex。Escape 鍵關閉最上層的覆蓋層。拖放與手勢操作必須有單一指標的替代方案 [2.5.7, 2.5.1]。
可見焦點 [2.4.7, 1.4.11]。使用 :focus-visible 樣式;絕不可以在未經驗證替代方案(與相鄰顏色對比度 ≥ 3:1)的情況下使用 outline: none。焦點不能被固定頁首或橫幅遮擋 [2.4.11]——需設定 scroll-padding-top。
焦點管理。 對話框:開啟時焦點移入、焦點循環、背景設為 inert、關閉時焦點回到觸發元素。SPA 路由變更:更新 document.title [2.4.2],並將焦點移至新的 <h1>(設定 tabIndex={-1})或 <main>。
表單 [1.3.1, 3.3.1, 3.3.2]。每個控制項都必須有可見的 <label>——佔位文字 (placeholder) 不是標籤。使用正確的 type、inputMode、autoComplete [1.3.5]。錯誤訊息內嵌顯示,使用 aria-describedby 關聯,錯誤欄位設定 aria-invalid="true",提交時焦點移至第一個無效欄位。在提交時驗證——不要等到輸入有效才啟用提交按鈕。絕不禁止貼上 [3.3.8]。
宣告動態變更 [4.1.3]。特定欄位使用 aria-describedby;非緊急(已儲存、結果數量、載入中)使用 role="status";緊急的全表單錯誤使用 role="alert"。動態區域 (live region) 必須在文字變更之前就已經存在於 DOM 中。
色彩與對比度 [1.4.3, 1.4.11, 1.4.1]。文字 ≥ 4.5:1(大字體 ≥ 3:1),UI 邊界、圖示與焦點環 ≥ 3:1——透過設計 Token 強制執行。色彩永遠不能作為唯一的提示。
觸控目標 [2.5.8]。尺寸 ≥ 24×24 CSS px 或間距足夠;桌面建議 40 px、觸控建議 44 px。裝飾性覆蓋層設定 pointer-events: none。
動畫與時間 [2.3.3, 2.2.2]。動畫透過 @media (prefers-reduced-motion: no-preference) 選擇啟用;在減少動畫設定下關閉視差/自動播放。任何持續超過 5 秒的動畫都必須可以暫停。含有動作或錯誤的提示訊息需持續顯示直到被關閉。
縮放與回流 [1.4.4, 1.4.10]。在 200% 文字大小與 320 CSS px 寬度下仍能正常運作,無橫向捲軸。絕不使用 maximum-scale=1 / user-scalable=no。
頁面基本要求。 <html lang> 設定完成並隨 UI 語言更新 [3.1.1];有一個可見的 <main>;標題形成大綱;當導航區位於 <main> 之前時,第一個焦點元素為跳過鏈結 [2.4.1]。
圖片。 有資訊價值的圖片:alt 描述其目的;裝飾性圖片:alt="";功能性圖片(圖片即連結/按鈕):描述其動作 [1.1.1]。
測試職責
每次 UI 變更都必須附帶:
- 元件測試:以角色與名稱查詢 (
getByRole),並涵蓋鍵盤路徑(user.tab()、{Escape}、toHaveFocus()) - Storybook 故事:addon-a11y 檢查設定為 error(會導致 CI 失敗)
- Playwright +
@axe-core/playwright掃描(WCAG 2.x A/AA 標籤)涵蓋關鍵頁面與狀態 - 使用手動鍵盤完整操作流程;關鍵流程需通過螢幕閱讀器測試(VoiceOver/NVDA)
自動化工具只能發現部分問題——乾淨的 axe 報告不等於符合 WCAG。詳細說明請見 references/testing.md。
完成前——掃描變更中的問題
| 模式 | 修正方式 |
|---|---|
<div onClick / <span onClick | 改為 <button>,或若是導航則改為 <a href> |
role="button"/role="tab" 但無鍵盤處理 | 使用原生元素或 APG 鍵盤對應 |
outline: none / outline-none 但無 focus-visible 替代 | 保留瀏覽器預設環或經驗證的自訂環 |
正數 tabIndex | 修正 DOM 順序,使用 0 / -1 |
<img 沒有 alt | 加上 alt="" 或目的描述 |
| 圖示按鈕沒有名稱 | 加上 aria-label,圖示設定 aria-hidden |
輸入框沒有 <label> / 以佔位文字作為標籤 | 加上可見的關聯標籤 |
disabled={!isValid} 作用於提交按鈕 | 保持啟用,在提交時驗證 |
{msg && <div role="status">…} | 先渲染空的區域,再更新其文字 |
成功訊息使用 aria-live="assertive" | 改為 role="status" |
aria-hidden 作用於或包住可聚焦元素 | 移除它或將子樹設為 inert |
onPaste + preventDefault | 允許貼上 |
maximum-scale=1 / user-scalable=no | 移除 |
固定頁首但無 scroll-padding-top | 在捲動容器加上 padding |
| 拖放/手勢無按鈕替代方案 | 增加單一指標路徑 |
transition: all,動畫無減少動畫保護 | 列出特定屬性,加上 no-preference 條件 |
報告方式
稽核與審查使用對話語言進行報告,每個根因 (root cause) 一項發現,包含所有位置、WCAG 標準、對使用者的影響及具體修正方式——格式請見 references/audit.md。嚴重程度對應程式碼審查技能:HIGH → Blocker/Major、MEDIUM → Major/Minor、LOW → Minor。
參考來源
綜合自:Addy Osmani 的 web-quality-skills accessibility(MIT 授權)、Jakub Krehel 的 better-accessibility(MIT 授權)、ibelick 的 fixing-accessibility(MIT 授權)、Vercel 的 Web Interface Guidelines(MIT 授權);W3C WCAG 2.2 與 WAI-ARIA Authoring Practices。