昨天那條線畫在「機器抓得到/抓不到」上,人力被推到剩下的三塊:鍵盤、焦點順序、螢幕報讀器。今天要修正那條線的位置——它不是固定的。W3C AT Driver、Guidepup 這幾年把螢幕報讀器自動化推到可用邊緣,線確實往下移了一格。但真正查下去會發現一件反直覺的事:把螢幕報讀器接進 CI,並不會幫你測到鍵盤。這兩件事被放在同一句話裡講了太多年,實際上是兩條互不相交的軸。分不清楚,就會花三個月建報讀器自動化,然後發現焦點順序的 bug 一個都沒被擋下來。
📖 學
報讀器自動化 2026 年的實際位置
先講基礎設施。ARIA-AT 是 W3C 的社群小組,目標是「輔助科技互通性」——建一套測試套件,讓螢幕報讀器、瀏覽器、新的 Web 技術的開發者都能確認同一個 ARIA 模式在不同組合下的行為是否一致。這件事本身是好事,但它的規模注定需要自動化,於是長出了 AT Driver:一份定義 JSON-over-WebSocket 雙向協定的規格,2024 年以 W3C Draft Community Group Report 發布,讓程式可以啟動/關閉報讀器、改設定、以及最關鍵的——讀到報讀器實際唸出來的那串字。
成果已經反饋到 APG。ARIA Authoring Practices Guide 上現在掛著「Assistive Technology Support」表格,標出各模式在 JAWS/NVDA/VoiceOver 上的支援度百分比,第一批涵蓋 Button、Link、Radio Group、Alert 四個模式。數字本身就很說明問題:Button 這種最基本的模式,Chrome + JAWS 是 100%、Chrome + NVDA 是 96%、Safari + VoiceOver 是 88%。一個 button 的行為在三個主流組合上都不完全一致——這就是為什麼「我在自己機器上用 VoiceOver 試過了」不能當作結論。
要在專案裡真的跑起來,目前最成熟的入口是 Craig Morten 的 Guidepup,支援 NVDA 與 macOS VoiceOver,還附一個純 JavaScript 寫的 virtual-screen-reader 模擬器。實務限制要先知道:VoiceOver 測試只能在 macOS 跑、NVDA 只能在 Windows 跑,兩邊的環境設定都涉及權限敏感的步驟(等於要跟資安部門先打招呼)。模擬器免設定,但作者自己在 README 裡就寫了它不能取代真實報讀器。
反直覺的那件事:報讀器自動化測不到鍵盤
Assistiv Labs 做過一份很誠實的分析:拿 Guidepup 對 guidepup.dev 寫的一組報讀器自動化測試,逐條對照 WCAG 2.2 AA 的 55 條成功準則,看它跟 axe-core 掃描各自能驗到什麼。結論裡最刺眼的一組是這幾條:
| 成功準則 | 報讀器自動化 | axe-core |
|---|---|---|
| 2.1.2 No Keyboard Trap (A) | ❌ | ❌ |
| 2.4.3 Focus Order (A) | ❌ | ❌ |
| 3.2.1 On Focus (A) | ❌ | ❌ |
| 1.4.13 Content on Hover or Focus (AA) | ❌ | ❌ |
原因寫得很直接:那組測試用的是 NVDA 的虛擬游標在頁面上移動,而虛擬游標不是鍵盤操作。虛擬游標走的是報讀器自己建的一份文件快取,它不會觸發 :hover、不會觸發 :focus、也不會遇到鍵盤陷阱。你可以寫一支測試確認 NVDA 唸出的標題結構完全正確,同時頁面上有一個 modal 會把 Tab 鍵吃掉——測試全綠。
所以正確的心智模型是兩條軸:「報讀器唸得對不對」是語意軸,「按鍵到得了嗎」是操作軸。語意軸的自動化正在成熟;操作軸的自動化仍然靠 Playwright 這類一般的端對端工具去驅動真實鍵盤事件,跟報讀器沒關係。昨天講的 axe-core 57% 天花板是語意軸的天花板;操作軸從頭到尾就沒被那個數字涵蓋過。
同一份分析還有第二個提醒:axe 對任何頁面開箱即用,報讀器自動化則必須為特定頁面手寫測試。它斷言的是「NVDA 唸出來的這串字跟上次一樣」,價值幾乎全部在回歸偵測而非發現既有 bug。如果現在的報讀器體驗就是壞的,寫測試固定下來,你得到的是一份被鎖住的壞體驗。Assistiv 自己的結論很保守:對多數只測自己網站的團隊,單靠報讀器自動化的覆蓋率不足以撐起那筆撰寫成本。
四層節奏,各自的頻率與角色
把上面兩件事合起來,2026 年多數團隊實際收斂到的節奏長這樣:
| 層 | 做什麼 | 頻率 | 抓什麼 |
|---|---|---|---|
| accessibility tree lint | 靜態+渲染後 DOM 掃描(axe-core) | 每個 PR | 語意結構、對比、標籤缺漏 |
| AT-driver 測試 | 對代表性頁面斷言報讀器輸出 | CI(每次合併) | 報讀器輸出的回歸 |
| 真人報讀器走查 | 人開 NVDA/VoiceOver 實際操作 | 每個 sprint | 語意是否「有用」而非「存在」 |
| 身障使用者稽核 | 由實際使用輔助科技的人測 | 每季或每年 | 前三層全部看不到的真實障礙 |
第三層最常被砍,所以理由要講清楚。機器能驗 alt 屬性存在、不是空字串,但只有人能判斷這段 alt 有沒有用。aria-label 唸出來是錯的、表單標籤唸起來像亂碼——這幾類的共同點是「技術上合規、實際上不能用」,永遠只能人驗。第四層更不可替代:熟練的 NVDA 使用者操作速度是設計師的好幾倍,他們三秒內遇到的障礙,你花二十分鐘也遇不到。
鍵盤那一軸:WCAG 2.2 把模糊變成可量測
操作軸過去難以進 CI,很大一部分是因為它的標準太模糊。舊的 2.4.7 Focus Visible 只要求焦點「可見」,沒有任何量測方式——這種條文沒辦法寫成測試,只能寫成「請設計師看一下」。WCAG 2.2 補了兩條新的把它拆開:
2.4.11 Focus Not Obscured (Minimum),AA 級:元件取得鍵盤焦點時,不得被作者建立的內容完全遮蔽。這條是為了 sticky header、cookie banner、聊天泡泡這類東西——它們最愛做的事就是在你 Tab 到下一個元素時,剛好把它蓋住。最省事的修法是 CSS:
html {
scroll-padding-top: 80px; /* sticky header 高度 */
scroll-padding-bottom: 96px; /* 聊天泡泡 + 底部 bar */
}瀏覽器自動把焦點捲進視窗時會留出這段空間。這是少數「改一行擋掉一整類 bug」的例子。
2.4.13 Focus Appearance,AAA 級:焦點指示器的面積,至少要等於在未聚焦元件外圍畫一圈 2 CSS 像素粗邊框的面積;而且聚焦/未聚焦兩個狀態的同一批像素,對比至少 3:1。這條給了可計算的門檻。注意網路上流傳兩套數字(「1 CSS 像素周長/最短邊 4 CSS 像素」是早期草案的措辭),引用前先確認版本;順帶一提,2.4.11 也常被誤寫成 Focus Appearance,因為草案期編號動過。
焦點面積怎麼算
拿一個 96×40 的按鈕實算一次。門檻是「2px 粗的外環面積」:
- 門檻 = (96+4) × (40+4) − 96 × 40 = 4400 − 3840 = 560 px²
三種常見寫法:
outline: 1px solid,無 offset → (96+2)×(40+2) − 3840 = 4116 − 3840 = 276 px²,只有門檻的 49%,不過。outline: 2px solid,無 offset → 560 px²,剛好等於門檻,過但沒有餘裕。outline: 2px solid; outline-offset: 2px→ (96+8)×(40+8) − (96+4)×(40+4) = 4992 − 4400 = 592 px²,592 ÷ 560 = 106%,過。
結論可以直接寫進設計系統的 token:焦點環最小 2px、offset 2px。順帶解決另一件事——offset 讓焦點環不會壓在元件邊界上,跟元件自身邊框的對比也更容易達到 3:1。
人驗的時間預算怎麼估
昨天引的 WebAIM Million 2026 有個數字:每頁平均 1437 個元素。就算只有 3% 可聚焦,也是大約 43 個 tab stop。每個停留 8 秒檢查「看得到焦點嗎、位置合理嗎、唸得對嗎」,純 Tab 一遍就要 344 秒,接近 6 分鐘;加上表單填寫、modal 開關、下拉展開,一頁的完整鍵盤走查抓 12 分鐘是務實的。代表性頁面取 8 頁,一個 sprint 的成本是 96 分鐘——一個半小時。
算這個數字的目的是拿去談判。「無障礙人工測試」聽起來像無底洞,「每 sprint 1.6 小時、涵蓋 8 條主要動線」則是可以被排進 sprint 的工項。把不可估的變成可估的,比爭論它重不重要有效得多。
🧠 記
語意軸和操作軸是兩件事。 螢幕報讀器自動化驗的是「唸出來對不對」,鍵盤測試驗的是「按得到嗎」。NVDA 虛擬游標不觸發 hover、不觸發 focus、不會撞到鍵盤陷阱,所以 2.1.2、2.4.3、3.2.1 這幾條在報讀器自動化和 axe 兩邊都是 ❌。買了報讀器自動化不等於買了鍵盤覆蓋,這筆帳要分開記。
報讀器自動化的價值在回歸,不在發現。 axe 對任何頁面開箱即用,報讀器測試必須逐頁手寫。它斷言的是「跟上次一樣」——所以先用人驗確認現況是對的,再寫測試把它鎖住;順序反了,你鎖住的是一份壞掉的體驗。這也決定了它該用在哪:登入、結帳、搜尋這種一壞就致命、而且很少改的核心動線。
模糊的標準進不了 CI,可量測的才行。 2.4.7 只說「焦點可見」,這種條文只能靠人主觀判斷;2.4.11 說「不得被完全遮蔽」、2.4.13 給了面積與對比公式,就能寫成測試、寫成 token、寫成 lint 規則。每次遇到「大家都同意但沒人執行」的規範,先問它能不能被量測——不能的話,缺的通常不是意志力而是定義。
✍️ 實踐
今天 20 分鐘,做一次單頁鍵盤走查,產出一份可重複的腳本。
- 選頁(1 分鐘):挑一條最重要的動線起點——登入頁或結帳第一步。
- 加防呆 CSS(2 分鐘):在全域樣式加
html { scroll-padding-top: <你的 sticky header 高度>; }。這步先做,因為它會直接消掉走查中一整類的假陽性。 - 拔掉滑鼠(12 分鐘):從網址列按 Tab 進入頁面,一路 Tab 到底,每一站記三件事——焦點看得到嗎?順序跟視覺閱讀順序一致嗎?有沒有 Tab 進去出不來的地方?遇到 modal 就開一次關一次,確認關閉後焦點回到觸發它的按鈕。把發現的問題直接記成 issue,標題前綴用準則編號(
[2.4.3]、[2.1.2])。 - 算一次焦點環(3 分鐘):量你主要按鈕的尺寸,套上面那個公式,確認焦點環面積過不過 560 px² 那類門檻。不過就把 outline 調成
2px+offset 2px,改在 token 層,一次修好所有元件。 - 存成模板(2 分鐘):把步驟 3 的三個問句存成 checklist,命名「鍵盤走查 v1」,下個 sprint 直接複製。
不要今天做的事:不要今天就去裝 Guidepup。先累積兩三次人工走查、確認現在的報讀器體驗是對的,再考慮把哪一條動線鎖進自動化。
🔗 延伸學習
- w3c/at-driver — AT Driver 協定本體,定義了 JSON-over-WebSocket 的報讀器遠端控制與輸出讀取,2024 年發布為 W3C Draft Community Group Report。
- Automating Screen Readers for Accessibility Testing — Assistiv Labs — 逐條對照 WCAG 2.2 AA 的 55 條準則,說明報讀器自動化與 axe-core 各自覆蓋到什麼、為什麼虛擬游標測不到鍵盤。
- Guidepup — 目前最實用的報讀器自動化入口,支援 NVDA 與 macOS VoiceOver,另附 virtual-screen-reader 模擬器與環境設定指南。
- WCAG 2.2 — SC 2.4.11 Focus Not Obscured (Minimum) — WCAG 2.2 新增的 AA 準則原文與說明,處理 sticky header 與浮動元件遮蔽焦點的問題。
- Managing Focus and Visible Focus Indicators — Vispero — 焦點管理的實務指南,涵蓋 modal、路由切換、動態內容的焦點該往哪送。
💬 問 AI
我們的 Web 產品(React + TypeScript)已經有 axe-core 在 CI 上跑每個 PR。
現在要補上「機器抓不到」的那一層,但預算有限,需要先把力氣放對地方。
請幫我:
1. 把 WCAG 2.2 AA 的成功準則分成三組——
(a) axe-core 這類靜態/渲染後掃描抓得到的
(b) 螢幕報讀器自動化(AT Driver / Guidepup)抓得到的
(c) 兩者都抓不到、只能靠鍵盤端對端測試或人工走查的
每組列出準則編號與一句話理由,並特別標出「常被誤以為屬於 (b)、其實屬於 (c)」的項目。
2. 針對 (c) 裡的鍵盤部分,給我一份 Playwright 測試模板,
涵蓋:Tab 順序符合 DOM 順序、無鍵盤陷阱、modal 開啟時焦點移入且關閉後歸位、
skip link 有效。用最小可用寫法,並說明每支測試對應哪條準則。
3. 設計一份「單頁鍵盤走查」的人工 checklist,
明確排除所有 axe 已經覆蓋的項目,只留需要人類判斷的題目,
每題附判斷準則(什麼情況算過、什麼情況算不過),並估算單頁執行時間。
4. 我們的設計系統焦點環目前是 outline 1px、無 offset。
請用 WCAG 2.2 SC 2.4.13 的面積與對比公式,
對 32px 高的小按鈕、40px 高的主按鈕、56px 高的大按鈕各算一次,
給出通過所需的最小 outline 寬度與 offset,並寫成 CSS 變數。
5. 給我一個判斷準則:什麼樣的頁面/動線值得投入寫報讀器自動化測試,
什麼樣的不值得。用可檢核的條件表達,不要只給原則。
請用繁體中文、台灣用語回答,程式碼給最小可用版本。