[note] Accessibility, a11y
此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料。
Accessibility Standards
Understanding the Web Content Accessibility Guidelines @ MDN
Accessibility 的標準最主要由 Web Content Accessibility Guidelines (WCAG) 加以定義和規範,並根據 conformance level 可以分成三個等級:A, AA, AAA (highest)。
除了 WCAG 之外,Web Accessibility in Mind (WebAIM) 則提供了 checklist 可以在實作時參考。
Screen Readers
透過 Screen readers 可以將文字轉成語音,讓使用者可 以聽到網站中的文字內容,並透過鍵盤來瀏覽頁面。
使用 Mac 上的 VoiceOver
Mac 上的 VoiceOver 就是一個相當完整的 Screen reader,只需到 System Preference -> Accessibility -> VoiceOver 即可啟用它。或者也可以使用快捷鍵「CMD + 點擊 3 下 TouchID」:
Accessible HTML
在學習下述這些屬性是非常建議參考已經實作好 a11y 的框架來搭配學習,例如 bootstrap。
Image
在使用 <img />
標籤的時候盡可能搭配 alt
屬性,如果不知道要寫什麼,留空白也比不加好:
<!-- screen reader 會閱讀 alt 中的文字 -->
<img src="https://example.com/foo.jpg" alt="An example image" />
<!-- alt 留空字串的話 screen reader 會跳過該圖片 -->
<img src="https://example.com/foo.jpg" alt="" />
<!-- 沒有 alt 的話 screen reader 會惱人的把網址唸出來 -->
<img src="https://example.com/foo.jpg" />
Label
label @ MDN
label 除了可以最常搭配 input 使用外,實際上也可以作用在其他 "labelable" 的元素,例如 <button>
、<select>
、<textarea>
、<meter>
、等等...,可以進一步參考 Accessibility concerns @ MDN。
ARIA and roles
aria-label, aria-labelledby, aria-describedby
對於不能使用 label 但又希望能提供 screen reader label 功能的元素(或者對於沒有文字內容的 button),則可以使用 aria-label
,例如:
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
aria-label
和 aria-labelledby
的功能相同,但後者需要對應到某個 id
,例如:
<div aria-labelledby="exampleModalLabel" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<!-- aria-labelledby 對應到這裡的 id -->
<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">...</div>
<div class="modal-footer">...</div>
</div>
</div>
</div>
而 aria-labelledby
和 aria-describedby
的差別在於,describeby 通常是用來放一些延伸說明。
role attribute
瀏覽器預設就已經針對不同 role
設定了不同相關的 accessibility 項目,所以如果我們真的需要把某些元素套用不同預設的 a11y 時,就可以擅用 role
這個屬性:
<!-- 讓 a 帶有 button 預設的 a11y 效果 -->
<p>
This <a href="#" role="button">button</a> triggers a popover on click.
</p>
visually-hidden, visually-only-focusable
可以參考 Bootstrap visually hidden content 中的 visually-hidden
和 visually-hidden-focusable
這兩個 class 的實作。
在 Bootstrap v4 時,這兩個 class 是被稱作 sr-only
和 sr-only-focusable
有些頁面上的內容會希望從畫面中隱藏,但仍希望 scree readers 可以閱讀相關的資訊時,則可以使用 sr-only
,例如用了額外說明顏色表示的訊息:
<!-- https://getbootstrap.com/docs/5.1/getting-started/accessibility/ -->
<p class="text-danger">
<!-- 使用 sr-only 來提示視覺障礙的使用者這是須留意的操作 -->
<span class="visually-hidden">Danger: </span>
This action is not reversible
</p>
除了單純使用 visually-hidden
之外,如果希望某些元素是在 focus 是會顯示,但一般的時候不會顯示的話,則可以使用 visually-hidden-focusable
:
<a class="visually-hidden-focusable" href="#content">Skip to main content</a>
這兩個 class 的實作是:
.visually-hidden,
.visually-hidden-focusable:not(:focus):not(:focus-within) {
position: absolute!important;
width: 1px!important;
height: 1px!important;
padding: 0!important;
/* 透過 margin: -1px 把 width, height 各為 1px 的元素隱藏 */
margin: -1px!important;
overflow: hidden!important;
clip: rect(0,0,0,0)!important;
white-space: nowrap!important;
border: 0!important;
}
Focus Management
Skip link
許多網站都會在網頁最上方提供 skip links 的功能,它的目的是讓鍵盤瀏覽網頁的使用者能夠避免要一直點擊 Tab 以跳過上方 header 或左側導覽列,才能進到主要內容。
要做 Skip link 可以直接參考 Bootstrap 的官方網站,基本上就是使用 visually-hidden-focusable
就能達到。
Keyboard & Tab Navigation
Keyboard @ MDN > WCAG Guidelines
在使用鍵盤瀏覽網站時,最基本的來說,使用 「Tab」 可以到下一個 tabbable item,使用 「Shift + Tab」 則可以到前一個 tabbable item。
但對於有選項後可以被展開的項目來說,後續則是使用「上下左右」鍵來操作:
對於任何可以被鍵盤所 focused 的元素來說,它都應該要是有互動意義存在的,也就是說,如果這個元素能被 focused,它就應該要有額外可被互動,像是能夠打開其他連結、或展開下拉選單等等。
任何可以被點擊(clickable)或可互動的元素,也都應該要能被鍵盤所 focusable,並透過 keyboard event 讓它帶有互動上的意義。
對於預設不能被 focus 的元素,可以使用 tabindex
這個屬性(參考:TabIndex and Focus styles @ pjchender),但要確保這個 focusable 的元素有其互動上的意義(可能需要額外處理 keyboard event),同時記得要為它添加 :focus
的樣式。
<!-- https://learn-a11y.netlify.app/accessible-html/index.html -->
<div
aria-label="Alert the word hello"
tabindex="0"
role="button"
onclick="alert('hello')"
onkeyup="alert('hello')"
>
Click me!
</div>
Active Element
使用 document.activeElement
可以取得使用者目前 focus 的元素;使用 .focus()
則可以讓特定的「focusable 元素」被 focus:
const currentFocusElement = document.activeElement;
focusableElement.focus();
focusableElement.focus()
只能用在「能被 focus 的元素」上(包含使用了 tabindex="0"
的元素)。
Tab Trapping
Tab Trapping 的技巧常用在 Modal 中,在 Modal 中,使用者在操作鍵盤的時候,並不會 focus 到 Modal 以外的元素(雖然預設是可以的)。實作的方式可以是監控 Modal 中最後一個可被 focus 的元素,當使用者在該元素上點擊 Tab 時,則 focus 回 Modal 中的第一個元素。
如下圖所示,focus 的元素只會維持在 modal 內:
Visual Considerations
Dark Mode: Prefers Color Scheme
告知瀏覽器(User Agent)支援的色系:color-scheme
另外可以在 HTML 的 <meta>
告知瀏覽器這個網站支援了那些 color-scheme,瀏覽器也會借此調整 UA(User Agent)預設的樣式:
normal
:頁面內的元素沒有針對 color-scheme 進行優化,因此會使用瀏覽器預設的 color-schemedark
:頁面內的元素可以支援 OS 中的 dark modelight
:頁面內的元素可以支援 OS 中的 light mode
<!-- 兩個都給表示兩個都支援,但更偏好前者 -->
<meta name="color-scheme" content="dark light">
類似的效果也可以寫在 CSS 的 :root
,但由於 CSS 的解析通常需要在 HTML 之後,所以更偏好直接寫在 HTML 的 <meta>
中:
/* 亮/暗色都支援,但更偏好暗色 */
:root {
color-scheme: dark light;
}
一般來說,可以直接把 User Agent(UA)當成瀏覽器的代名詞。
取得使用者系統偏好的主題:window.matchMedia + prefers-color-scheme
最重要的關鍵是使用 window.matchMedia('(prefers-color-scheme: dark)');
來了解使用者在系統偏好設定中,是選擇的亮色或暗色主題:
const defaultTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
也可以透過 checkbox 讓使用者切換主題:
/** query DOM elements */
const rootElem = document.querySelector('body.main');
const checkboxElem = document.getElementById('input');
/** get theme the user preferred */
const defaultTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
let currentTheme = defaultTheme;
/** toggle the theme */
const handleToggleTheme = (e) => {
if (e.target.checked) {
currentTheme = 'dark'
} else {
currentTheme = 'light'
}
rootElem.setAttribute('data-theme', currentTheme);
}
const init = () => {
checkboxElem.checked = currentTheme === 'dark';
checkboxElem.addEventListener('change', handleToggleTheme);
rootElem.setAttribute('data-theme', currentTheme);
}
init()
搭配 data-theme 和 CSS variable:
:root {
--background: white;
--text-primary: black;
--text-secondary: royalblue;
--accent: purple;
}
[data-theme='dark'] {
--background: black;
--text-primary: white;
--text-secondary: grey;
--accent: darkred;
}
.main {
background: var(--background);
color: var(--text-primary);
}
button {
color: var(--text-primary);
background-color: var(--background);
border: 2px var(--text-primary) solid;
float: right;
transition: all .5s;
}