iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Modern Web

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

Day 13:`outline: none` 我一行都沒寫,焦點還是有兩個地方看不見

  • 分享至 

  • xImage
  •  

Day 13 · W2 · 無障礙線 · 難度 ★★★☆☆

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

我沒有在任何一支樣式表裡寫過 outline: none。自家站每一個能用 Tab 停到的地方,都畫了一圈 2px 的外框。

昨天我把鍵盤按過去,一個一個量。33 個停留點裡,有兩個的外框,眼睛看不見。

一句話主軸:焦點外框畫在元件外面,所以它要跟旁邊的顏色比,不是跟元件自己的填色比。

那圈外框要跟誰比,outline-offset 說了算

WCAG 2.4.13 焦點外觀的原文寫得很精確,它要求的對比是:

has a contrast ratio of at least 3:1 between the same pixels in the focused and unfocused states

同一批像素,比它「聚焦前」跟「聚焦後」的顏色,要到 3:1。

所以真正的問題不是「外框是什麼顏色」,是那圈外框佔用的像素,在聚焦之前是什麼顏色

outline-offset:  2px;   /* 環畫在元件外面 → 那些像素本來是頁面底色 */
outline-offset: -2px;   /* 環壓在元件裡面 → 那些像素本來是元件的填色 */

判準短到可以背:offset 為正就比外面,為負就比元件自己。

我第一次量錯了。原本以為「元件的背景」就是那個要比的對象,於是初測一律拿元件自己的填色去比,深色帶上的藍色按鈕算出 1.00,等於在說這圈外框完全不存在。結果打開瀏覽器一看,那圈框清清楚楚,因為它根本沒畫在按鈕上,它畫在按鈕外面。

這跟 Day 10 是同一類錯誤:挑錯了要拿來比的那個背景。Day 10 的版本是漸層文字,這次換成焦點環,公式沒變,變的是「哪一層才是背景」。

半透明的柔和,柔到看不見

第一個掉在線下的是搜尋框,1.54。

.SiteSearch_input:focus {
  outline: rgba(10, 71, 176, .25) solid 2px;
  outline-offset: 2px;
}

那個 .25 是刻意的。設計上不希望搜尋框被 Tab 到就跳出一圈死藍,所以把品牌藍調淡。

問題是 CSS 裡寫的 rgba(10, 71, 176, .25) 不是螢幕上的顏色。它疊在頁面底色 #fcfdfe 上之後,實際呈現的是 rgb(192, 208, 234),對比 1.54

在白牆上用白色麥克筆畫記號 —— 筆有下去,痕跡也在,就是看不到。

半透明是焦點框最容易踩的一格:你在編輯器裡看到的是那串 rgba,使用者看到的是合成之後的結果。這件事 Day 10 講的是文字,搬到外框上一模一樣。

同一個藍,兩種命運

第二個是深色價格帶裡的按鈕,1.93。它用的樣式很正常:

a.button:focus-visible {
  outline: rgb(10, 71, 176) solid 2px;
  outline-offset: 2px;
}

一行 CSS,全站 33 個停留點跑出來的分佈是這樣:

8.11  ×20     白底區塊
7.64  ×11     淺灰底區塊
1.93  ×1      深色價格帶上的按鈕
1.54  ×1      搜尋框

品牌藍對白底 8.11,非常安全。同一個色票,掉到 rgb(32, 34, 37) 的深色帶上就變成 1.93。

這張表還有一件事值得看:33 個停留點全部都有可見的焦點指示,一個都沒漏。也就是說 2.4.7「焦點看得見」這條,我的站是全過的。掉在線下的那兩個,是 2.4.13 才管得到的層次。

有沒有畫,跟看不看得清楚,是兩個不同的準則。 只跑前面那一條,你會拿到一份漂亮的全綠報告。

焦點外框的顏色不能只挑一次。 它得在頁面上所有它可能出現的底色上都成立,而一個站通常有淺底、有深底、有圖片上的浮層。

這也是為什麼規範建議的做法叫「雙色焦點指示器」:內外兩圈不同明度,深底淺底至少有一圈活著。

實務上還有一招更省事:外框不挑顏色,直接用 currentColor 或反色,讓它跟著文字走。文字本來就得對背景合格,外框借它的合格結果。缺點是品牌感會被稀釋,所以這是取捨不是標準答案。

「同一個色票兩種命運」這件事 Day 17 講深色模式時會再遇到一次。

焦點外框對比分佈圖:橫軸為對比值,標出 3:1 的門檻線;33 個 Tab 停留點中,白底區塊的 20 個落在 8.11、淺灰底區塊的 11 個落在 7.64,兩者都遠高於門檻;門檻線左側有兩個落點,深色價格帶上的按鈕為 1.93、搜尋框為 1.54,皆未達 3:1

兩個不合格的用的是同一個品牌藍。差別只在它們被畫在什麼顏色上面。

那串碼不是流水號,它把等級寫在裡面

先把一個詞釘住。這系列講的碼號,一律指 MODA 規範裡的檢測碼與稽核評量碼,不是 WCAG 的成功準則編號。兩種號碼常常並排出現,長得也不像:2.4.13 是 WCAG 的,CS3241300E 是碼號。

焦點這件事在碼表上不是一條,是七條,掛在四個成功準則底下:

2.4.7    CS2240700E   使其鍵盤焦點指示具高可見度
         FA2240701E   樣式的輪廓與邊框使焦點指示框線不可見
2.4.11   FA2241100E   黏性頁腳或頁首完全遮蔽了焦點元素
2.4.12   CS3241200E   使用滾動邊距(scroll-padding)來消除內容遮蔽
2.4.13   CS3241300E   建立雙色焦點指示器以確保與所有組件有足夠的對比度
         CS3241301E   在組件內建立強而明顯的焦點指示框線
         GN3241302E   使用作者提供的可見焦點指示框線

右邊那一串看起來像流水號。規範裡有一整節在定義它是怎麼組出來的。

稽核評量碼格式為3碼文字與7碼數值,如:XX3141099E,此10碼字符共可分為5項

五項是這五段:

CS      網頁科技
3       檢測等級
2413    成功準則
00      流水號
E       類型

每一段的可能值:

碼號結構拆解圖:以 CS3241300E 為例,十個字元分成五段。起始兩碼 CS 是網頁科技的編碼簡稱,其他常見值包括 HM 代表 HTML、GN 代表通用、FA 代表常見失敗、AR 代表 ARIA;第三個字元 3 是檢測等級,1 是 A、2 是 AA、3 是 AAA;中間四碼 2413 是對應的 WCAG 成功準則 2.4.13;接著兩碼 00 是同一條成功準則底下的流水號,從 00 起算;最後一碼 E 是類型,C 代表可用軟體檢測、E 代表需人工稽核。下方以 FA2241100E 示範:第三個字元是 2,所以它是 AA

中間四碼就是 WCAG 的成功準則號。碼號跟 WCAG 準則號會被搞混,是因為前者真的把後者包在裡面。

我照這份格式把 115.11 的 244 條碼號全部拆過一次,244 條全部解得開。規範自己也報了總數:可用軟體檢測的檢測碼 28 條、需人工稽核的稽核評量碼 216 條,加起來 244 —— 跟我手上那份碼表對得起來。

所以要知道一條是 A 還是 AAA,不用查表,看第三個字元就好。 焦點這一家七條裡,FA2241100E 的第三個字元是 2,AA。黏性頁首把焦點元素整個蓋掉,那不是加分題,是義務。

至於那些等級是怎麼來的:2.4.7 在 WCAG 2.1 與 2.2 都是 AA;2.4.11 是 2.2 新增的 AA;2.4.12 與 2.4.13 是 2.2 新增的 AAA。Day 6 已經確認過,換版是碼表位移,不是義務降級。

outline: none 不是罪,沒補回來才是

先幫這行 CSS 平反。當年大家會寫它,是因為 :focus 對滑鼠點擊一樣生效 —— 按鈕被點一下就套一圈框,設計師會抓狂。

:focus-visible 就是為了這件事存在:瀏覽器自己判斷這次聚焦要不要給視覺提示,鍵盤 Tab 給,滑鼠點不給。

button:focus { outline: none; }
button:focus-visible { outline: 3px solid #0b6b65; outline-offset: 2px; }

寫探針時我在這裡踩過一個坑:getComputedStyle(el, ':focus-visible') 是無效的,第二個參數只吃偽元素(::before 那種),不吃偽類。Chromium 不會報錯,它會安靜地退回非聚焦樣式,於是你得到一堆「這個元素沒有焦點指示」的假陰性。

正確做法是先按 Tab 讓元素真的聚焦,再讀它的一般計算樣式 —— 那時候該套的那一層已經在裡面了。

黏性頁首會把焦點整個蓋住

FA2241100E 對應的 2.4.11 是這一家裡最容易中的一條,因為它跟你的 CSS 寫得好不好無關。

瀏覽器會把聚焦的元素捲進可視範圍,但它不知道你的頁首是 position: sticky。捲到位之後,元素確實在畫面上,蓋在它上面的是那條頁首。

省事的解法只有一行:

:target, :focus-visible { scroll-margin-top: 5rem; }

探針怎麼判:拿聚焦元素的外框取四個點,看每個點最上層是哪個元素;如果那個元素的祖先鏈上有 stickyfixed,這一點算被蓋。四點全被蓋等於整個被蓋,那是 2.4.11 的 AA;只要有一點被蓋就違反 2.4.12 的 AAA。

這個分界不是我發明的,是準則自己的字面差別:一個寫 entirely hidden,一個寫 no part。

焦點外框與底色的關係示意圖:左半邊是 outline-offset 為正的情形,2px 的外框畫在元件邊界外側,該處像素在聚焦前是頁面底色,因此對比要拿外框顏色與頁面底色相比;右半邊是 outline-offset 為負的情形,外框壓在元件邊界內側,該處像素在聚焦前是元件自身的填色,因此要拿外框顏色與元件填色相比;下方標註同一個品牌藍在白底上得到 8.11、在深色帶上只有 1.93

聚焦前後同一批像素的對比。offset 的正負決定了「聚焦前」是誰。

我的工具只量了粗細那一半

CS3241301E 量的是外框夠不夠粗,門檻 2px。它從來沒量過對比。

而它報出來的訊息裡有這麼一句:「建議 outline 至少 2px,並與相鄰色彩具足夠對比。」

後半句是寫給人看的建議,程式一個字都沒有執行。 真正管對比的是 CS3241300E,碼表上有,我沒實作。更難看的是:探針早就把 outline_color 收進來了,一直放著沒有任何規則在用。

補完規則之後重掃,只抓到 1 個。另一個去哪了?

Tab 走訪在第 11 個停留點就停了。判斷「是不是繞回起點」用的 key 是 標籤|選擇器|文字 的組合,導覽列有好幾個文字相同的連結,第一個撞名就 break。33 個停留點只走了 11 個,後面的問題全部看不到。

改成兩個真訊號才停:焦點按了 Tab 沒有移動,或是回到第一個停留點。

這節的重點不是我補了一條規則。 是你手上那套自動化檢查,很可能也只量了粗細那一半,而它的輸出還會用「並與相鄰色彩具足夠對比」這種句子,把沒做的那一半蓋過去。

手動 Tab 一遍,看焦點停在哪、看不看得見。這件事目前沒有工具能完全代勞。

今天的重點

  • outline: none 沒寫,不代表焦點看得見,補了也不一定夠
  • 焦點外框要跟旁邊的顏色比:2.4.13 比的是同一批像素聚焦前後的差,outline-offset 的正負決定「聚焦前」是誰
  • 半透明最容易踩rgba(..., .25) 想柔和,合成之後只剩 1.54
  • 同一個品牌藍,白底 8.11、深色帶 1.93,所以規範才建議雙色指示器
  • 碼號第三個字元就是等級FA2241100E 是 AA,黏性頁首蓋住焦點是義務不是加分
  • 自動化只量得到粗細那一半,手動 Tab 一遍還是要做

明天 Day 14:我的工具會自己去按別人網站上的按鈕,所以我列了一份「不准按」的清單。然後我發現,真正會把東西送出去的那顆鍵,根本不在上面。


上一篇
Day 12:被我掃的網頁,可以叫我的工具說它通過
下一篇
Day 14:我列了一份「不准按」的清單,然後發現送出鍵不在上面
系列文
前端不寫 Python,照樣 ship 一把網頁無障礙 CLI15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言