iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Modern Web

前端不寫 Python,照樣 ship 一把網頁無障礙 CLI系列 第 5

Day 05:axe-core、pa11y、Lighthouse 都跑過了,為什麼還要再蓋一把

  • 分享至 

  • xImage
  •  

Day 05 · W1 · 無障礙線 · 難度 ★★☆☆☆

本系列由 AI 協作撰寫。 內容、技術判斷、程式碼由 light-design 數位顧問團隊與 Claude 共同產出,最終由作者驗證後 publish。完整協作模式與把關方式見 Day 01

Day 4 結尾我說這三個工具都跑過。今天把數據攤開。

先看一行 HTML:

<input type="text" placeholder="姓名">

axe-core 判「過」。a11y-moda 判「不過」。

兩邊都沒錯。 這篇就是在解釋為什麼會這樣,以及為什麼這正是我還要再蓋一把的理由。

一句話主軸:在台灣送件,你對的是碼表,不是條文。

「不要重複造輪子」是對的。但判斷重不重複之前,得先看兩個輪子裝在哪一台車上。

先講測試台,不然數字沒有意義

兩個目標,都可以重跑:

一、W3C WAI 的 Before and After Demonstration。 W3C 自己做的教學站,同一個網站有故意做壞的 before 版跟修好的 after 版,網址就是 w3.org/WAI/demos/bad/before/home.html/after/home.html

二、Day 4 那個八行 fixture。 我親手埋了六個問題進去。

我沒有拿任何營運中的網站當負面案例。一來不厚道,二來它隨時可能改版,讀者跟著跑會對不上。BAD 是公開、固定、W3C 本來就標明用途包含「拿工具評估」的素材,對四個工具一視同仁。

fixture 的作用不一樣:我知道裡面有什麼,所以能算召回率,而不只是比誰報得多。

四個工具掃同一頁,數字長這樣

a11y-moda scan <url> --level AA --render
npx pa11y --standard WCAG2AA --reporter json <url>
lighthouse <url> --only-categories=accessibility --output=json
# axe-core 走 Playwright 注入 axe.min.js 後 axe.run()
工具 before(故意做壞) after(W3C 修好版)
a11y-moda 11 fail · 6 info · 1 caveat 4 fail · 4 info · 1 caveat
axe-core 4.13 6 規則 / 46 節點 1 規則 / 2 節點
pa11y 41 則 0 則
Lighthouse 分數 50 · 7 項不過 分數 91 · 2 項不過

這張表不能直接比大小,因為單位不一樣。

pa11y 的 41 跟 axe 的 46 是同一量級,兩個都是逐元素算;axe 的 6 條規則跟我們的 11 條才是同一量級,兩個都是逐規則算。把 41 跟 6 放在一起說「pa11y 抓得比較多」,等於比較兩家醫院一年處理幾例,一家算病人數、另一家算科別數。

四個工具在同一頁的計數單位對照圖:左欄為逐元素計數,pa11y 41 則與 axe-core 46 個節點屬於同一量級;右欄為逐規則計數,axe-core 6 條規則與 a11y-moda 11 條規則屬於同一量級;Lighthouse 輸出的是 0 到 100 的分數,與前兩者都不同

同一頁、同一天跑出來的四組數字。要比之前得先把單位對齊,否則整張表都是誤導。

那個 <input>,兩邊都對

先確認 axe 不是沒跑到那條規則。我單獨指定規則再跑一次:

passes    label       nodes=1   <input type="text" placeholder="姓名">
passes    link-name   nodes=1   <a href="/pricing">按這裡</a>
violations color-contrast nodes=1  <p style="color:#999;background:#fff">小字說明</p>

label 那條是明確的 passes,不是未執行。

原因很正當:placeholder 依照 accessible name 的計算規格確實會成為那個輸入框的名字,所以 WCAG 4.1.2「名稱、角色、值」這關過得去。axe 沒有做錯任何事。

MODA 碼表的寫法不一樣。HM1130103C 的原文是「可見的表單控制元件均需有對應的標籤組件,或有標題屬性,且其內容或值均不得為空字串或空白」。它明文指定了實作方式,而不只是要求結果。placeholder 不是 <label>、也不是 title,所以我們 fail。

同一個輸入框,兩個工具給相反的答案,而兩邊都符合各自對的那份文件。

順帶一提,<a href="/pricing">按這裡</a> axe 也判過,因為 link-name 只查連結有沒有文字,不判斷那段文字講不講得清楚要去哪。這一格我們也沒抓到。這輪沒有開 LLM,數字全部是純機器規則的結果。

六個埋好的問題,誰抓到幾個

BAD 只能比「誰報了什麼」,比不了「誰漏了什麼」。要看漏抓,得用有標準答案的題目。

Day 4 那八行裡我埋了六個:圖片沒有 altdivonclick 沒有鍵盤事件、<html> 沒有 lang、輸入框只有 placeholder、連結寫「按這裡」、#999 配白底對比 2.85。

工具 alt 鍵盤 lang label 按這裡 對比 命中
a11y-moda 抓到 抓到 抓到 抓到 抓到 5/6
pa11y 抓到 抓到 抓到 抓到 4/6
axe-core 抓到 抓到 抓到 3/6
Lighthouse 抓到 抓到 抓到 3/6

三件事值得看。

四個工具都抓到的那三格(alt、lang、對比)是行業共識,不管你用哪一把都不會漏。這也代表如果你的網站連這三樣都在掉,換工具不會解決問題。

divonclick 沒有鍵盤事件只有我們報。 這條在碼表裡是 GN1210100E,明文要求滑鼠事件要有鍵盤等效。它不難檢查,只是不在其他三個工具的題目範圍裡。

「按這裡」四個全滅。 這格不是誰的疏失,是機器規則到不了的地方 —— 要判斷「這段連結文字有沒有講清楚要去哪」,需要理解語意。這正是那些人工判斷規則存在的理由,也是我們把 LLM 接進來的位置。

W3C 修好的那一版,四個工具都還有話說

after 版的殘留很有意思:

工具 還在報什麼 屬於
axe / Lighthouse target-size WCAG 2.2 的 2.5.8
Lighthouse landmark-one-main ARIA 慣例,非準則明文
a11y-moda 320px 下仍需水平捲動 WCAG 2.1 的 1.4.10
pa11y

我原本以為這是「我們比較嚴」,看完才發現不是。axe 跟我們指到的是同一件事:這一頁是 WCAG 2.0 時代做的。 2.5.8 是 2.2 才有的準則,1.4.10 是 2.1 才有的,BAD 當年沒有理由符合它們。

兩個工具從完全不同的角度得到同一個結論,這比我單方面宣稱「那份素材太舊」可信得多。

至於 pa11y 的 0,它的引擎只做到 WCAG 2.0,所以那個 0 是誠實的,在它的規範版本裡確實沒問題。

「0 個問題」有時候是引擎版本的資訊,不是網頁的資訊。 看到全綠先問一句:它在看哪一版。

只有我們抓到的那幾條,來自碼表不是能力

BAD 的 before 版裡,這四條其他三個工具都沒報:

rule_id WCAG 內容
CS1110114E 1.1.1 1×1 透明 GIF 用於版面,請改用 CSS
CS1130103E 1.3.1 使用已淘汰的 <font> 元素
HM1240404E 2.4.4 「Read More...」重複 2 次卻指向不同網址
GN1210100E 2.1.1 <td>onmouseover 但沒有 onfocus

這不是「我們比較強」。是 MODA 把準則具體化到了這個層級,而 WCAG 條文停在更上面一層。

看那條 <font> 就懂了。WCAG 1.3.1 要求的是「結構與關聯要能被程式判定」,它從頭到尾沒提過任何一個 HTML 元素的名字,因為條文要能適用二十年。碼表可以,也必須具體:它直接點名已淘汰的呈現性元素。axe 不報這個不是它漏了,是那條規則在它的世界裡沒有依據可以成立。

判定其實有三層,分清楚才不會吵錯架:

三層判定來源示意圖:最上層是 WCAG 準則條文,只給目標與少數數字;中間層是 MODA 檢測碼,把準則具體化成可檢測條件;最下層是工具的實作判定,決定觸發門檻。右側以輸入框為例,說明 WCAG 只要求有名稱,MODA 明文要求標籤組件或標題屬性,工具再決定何時觸發

三層一路往下越來越具體。吵「誰對」多半是因為兩邊站在不同層。

第三層是我們自己的,所以要自己招:GN1240103E 報「55 個連結未用結構性組件分群」,碼表寫的是「使用結構性組件來將鏈結分群」,那個 55 是我們挑的HM1130100C 報「不要超過一個 h1」,碼表只說「按照正確的巢狀層次結構」,單一 h1 那句也是我們加的

Day 4 那個 _MAX_LABEL_LEN = 16 一樣,都是寫死在原始碼裡、使用者改不動的數字。

這篇撐不起的四件事

主張只有一句「對的規範不同」。以下四件我不主張,寫清楚免得被讀成別的意思:

一、不是檢得比較準。 BAD 的 after 版我們報四條,裡面就有第三層那些自訂門檻。

二、不是覆蓋比較全。 我們的規則落在 110.07 碼表內的有 124 條,碼表總共 240 條 —— 52%,一半剛過。 剩下那 116 條有些是還沒寫,有些是機器本來就判不了,得靠人看畫面、聽報讀、實際操作一遍流程才有答案。所以「送件前跑一次工具就安心」這種話我講不出口,它頂多幫你把明顯的先清掉,讓人力留給真正需要判斷的部分。另外 axe 在 ARIA 那一塊比我們深得多,那是它十幾年累積的地方,我們短期內追不上,也沒打算追。

三、不是取代誰。 官方判定是官方的,我們是協助工具,碼表跟著公布版本走。

四、出了 MODA 送件情境,這個差異歸零。 如果你的網站不需要送台灣的無障礙標章,axe-core 是更好的選擇:社群大、更新快、生態成熟,我們沒有任何理由建議你捨它就我。

所以這到底算不算造輪子? 在 WCAG 的世界裡算,在 MODA 碼表的世界裡不算。 你在哪個世界,決定了這個答案。

今天的重點

  • 計數單位不同的工具,數字不能直接比。 逐元素跟逐規則差好幾倍,先對齊再說話
  • 「0 個問題」可能是引擎版本的資訊,不是網頁的資訊。看到全綠先問它在看哪一版
  • 同一個 <input>,axe 對、我們也對 —— 對的規範不一樣,這是規範層級差異不是工具高下
  • 判定分三層:WCAG 條文、MODA 碼表、工具自己的門檻。第三層要自己招
  • 52%、不比較準、不取代、出台灣歸零 —— 這四件事這篇撐不起來

明天 Day 6:那份碼表 11 月 30 日換版。我算出來要補 109 條,後來發現這個數字是錯的。


上一篇
Day 04:20 秒裝好,然後它抓到自己
下一篇
Day 06:規範換版對齊 WCAG 2.2,我以為要補 109 條,實際只有 7 條
系列文
前端不寫 Python,照樣 ship 一把網頁無障礙 CLI6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言