FE Day 28 無障礙(a11y)基礎
執行需求:CPU 可跑。昨天把 E2E 測試的 Playwright 工具鏈走完,今天進入另一個常被忽略的品質主題:無障礙(accessibility,簡稱 a11y)。對後端轉前端的工程師來說,無障礙聽起來像是「設計師的事」,但實際上 80% 的 a11y 問題出在 HTML 結構與互動邏輯:表單欄位沒 label、圖片沒 alt、互動元素不能鍵盤操作、ARIA 屬性用錯。前端工程師是第一線的把關者。今天會從 WCAG 2.2 的四項原則(Perceivable、Operable、Understandable、Robust)講起,接著示範怎麼用 React 19 + Next.js 15 寫出「鍵盤可達、螢幕閱讀器友善」的元件,並用 axe-core + Playwright 自動化掃 a11y 問題。
引言
無障礙設計(accessibility design)的目的是「讓所有人不論能力都能使用產品」。這包含視障、聽障、肢體障礙、認知障礙的使用者,也包含「暫時性障礙」的情境:手骨折只能單手操作、拿著咖啡無法精準點選、在吵雜環境用耳機聽螢幕閱讀器、網路慢圖片還沒載完。做好無障礙不只是道德或法律義務(很多國家有明確的法規要求),它也直接提升 SEO 與轉換率——Google 搜尋偏好語意清楚、鍵盤可達的網站,使用者在表單填到一半因為「找不到送出按鈕在哪」而放棄的機率會大幅下降。
WCAG 2.2(Web Content Accessibility Guidelines)是業界最通用的標準,把無障礙分成四個原則(POUR):Perceivable(可感知)、Operable(可操作)、Understandable(可理解)、Robust(強健)。每個原則底下有具體的 guideline,例如「所有非文字內容都有替代文字」、「所有功能可透過鍵盤操作」、「頁面標題與語言有正確標示」。2026 年 3 月主流的工具鏈是 axe-core(自動掃 a11y 問題的規則引擎)、Lighthouse(Google 的 a11y 分數)、Pa11y CI(CI 用的 a11y 測試)、NVDA / VoiceOver(螢幕閱讀器實測)。今天會把 axe-core 整合進 Playwright 跑在 CI。
今天的目標有四:第一,理解 WCAG 2.2 的四原則與具體檢查項;第二,學會用 semantic HTML 與 ARIA attributes 寫 a11y-friendly 的 React 元件;第三,用 axe-core + Playwright 自動化掃常見 a11y 問題;第四,知道哪些 a11y 規則最常被忽略、怎麼避開。讀完之後你應該能獨立判斷「這個 React 元件在螢幕閱讀器會不會出問題」,並能用工具自動掃出大部分問題。
WCAG 2.2 四原則與具體檢查項
POUR 是 WCAG 2.2 的最高層級抽象。每一項底下有 10-15 條 guideline,每條 guideline 有「可通過的測試方法」(test criteria)。要從頭讀完整份規格不切實際,我們只看最常見的 8 條檢查項,這 8 條涵蓋了 80% 的實際 a11y bug:
- Perceivable - 非文字內容有替代文字:
<img>一定要有alt;裝飾性圖片用alt="";icon 按鈕用aria-label。 - Perceivable - 色彩對比至少 4.5:1:一般文字對背景至少 4.5:1、大字(18pt 以上)至少 3:1。Tailwind 的
slate-600在white上是 5.7:1 通過。 - Operable - 所有功能可鍵盤操作:不用滑鼠也能完成所有操作。
div onClick不行,要用<button>。 - Operable - 焦點可見:鍵盤 focus 必須有視覺指示。Tailwind 的
focus-visible:ring-2是標準做法。 - Understandable - 表單欄位有 label:
<input>一定要有關聯的<label>;placeholder 不是 label。 - Understandable - 語言有宣告:
<html lang="zh-Hant">;切換語言的段落用lang="en"。 - Robust - HTML 結構合法:標籤正確嵌套、屬性值正確,例如
<a href>不能只放<div>充當連結。 - Robust - 狀態變化有宣告:modal 開啟用
role="dialog"、載入中用aria-busy、錯誤訊息用role="alert"。
這 8 條剛好對應 axe-core 預設會掃的規則集。axe-core 是Deque Systems 維護的開源規則引擎,瀏覽器擴充、Lighthouse、CI 都用它。我們等一下會把它整進 Playwright。
Semantic HTML 與 ARIA:寫對的標籤
無障礙的第一守則是「用對 HTML 標籤」。<div onClick={...}> 看起來跟 <button onClick={...}> 一樣,但對螢幕閱讀器來說差很多:螢幕閱讀器會把 <button> 識別為「按鈕,可以按 Enter 觸發」,但 <div> 只會被唸成「無意義容器」。React 的常見 a11y bug 幾乎都是「用了錯的標籤」。
正確的標籤選擇有一張對照表,新手可以先存起來:
| 情境 | 錯誤示範 | 正確示範 |
|---|---|---|
| 按鈕 | <div onClick> |
<button type="button"> |
| 連結 | <a onClick> |
<Link href>(next/link) |
| 導航 | <div>menu</div> |
<nav> |
| 主要內容 | <div>content</div> |
<main> |
| 頁首 | <div>header</div> |
<header> |
| 獨立區塊 | <div> |
<section> + aria-labelledby |
| 清單 | <div>item 1</div> |
<ul><li> |
ARIA(Accessible Rich Internet Applications)是 W3C 規範,補 semantic HTML 不足的部分。常用的 ARIA 屬性:aria-label(給元素一個可朗讀的名字)、aria-labelledby(指向現有元素當 label)、aria-describedby(指向 hint 或錯誤訊息)、aria-invalid(表單欄位錯誤狀態)、aria-busy(載入中)、role="alert"(重要訊息要立刻朗讀)。ARIA 的使用原則是「能不用就不用」,因為錯用 ARIA 比不用更糟——螢幕閱讀器會被誤導。
看一個按鈕的正確寫法。Day 32 寫的 Button 元件加上 a11y 細節:
// components/ui/Button.tsx(含 a11y 細節)
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { cn } from "@/lib/cn";
type Variant = "primary" | "secondary" | "ghost" | "danger";
type Size = "sm" | "md" | "lg";
type ButtonProps = ButtonHTMLAttributes&amp;lt;HTMLButtonElement&amp;gt; &amp;amp; {
variant?: Variant;
size?: Size;
loading?: boolean;
loadingLabel?: string;
children: ReactNode;
};
const variantClasses: Record&amp;lt;Variant, string&amp;gt; = {
primary: "bg-brand-600 text-white hover:bg-brand-700 focus-visible:ring-brand-500",
secondary: "bg-white text-text-primary border border-surface-border hover:bg-surface-muted focus-visible:ring-brand-500",
ghost: "bg-transparent text-text-primary hover:bg-surface-muted focus-visible:ring-brand-500",
danger: "bg-danger-500 text-white hover:bg-danger-600 focus-visible:ring-danger-500",
};
const sizeClasses: Record&amp;lt;Size, string&amp;gt; = {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-base",
lg: "h-12 px-6 text-lg",
};
export function Button({
variant = "primary",
size = "md",
loading = false,
loadingLabel = "載入中",
disabled,
className,
children,
...rest
}: ButtonProps) {
return (
&amp;lt;button
type="button"
disabled={disabled || loading}
aria-busy={loading}
aria-live="polite"
className={cn(
"inline-flex items-center justify-center gap-2 rounded-button font-medium",
"transition-colors disabled:cursor-not-allowed disabled:opacity-60",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
variantClasses[variant],
sizeClasses[size],
className,
)}
{...rest}
&amp;gt;
{loading ? (
&amp;lt;&amp;gt;
&amp;lt;span aria-hidden="true"&amp;gt;...&amp;lt;/span&amp;gt;
&amp;lt;span className="sr-only"&amp;gt;{loadingLabel}&amp;lt;/span&amp;gt;
&amp;lt;/&amp;gt;
) : (
children
)}
&amp;lt;/button&amp;gt;
);
}
這個 Button 有四個關鍵的 a11y 細節。第一,aria-busy={loading} 告訴螢幕閱讀器「正在執行,請稍候」。第二,aria-live="polite" 讓螢幕閱讀器在狀態變化時主動朗讀(例如「已變成載入中」)。第三,loading 時用 sr-only 把視覺的「…」藏起來、另外用一個視覺不可見的 span 告訴螢幕閱讀器「載入中」,避免「…」被朗讀出來讓使用者困惑。第四,focus-visible:ring-2 給鍵盤使用者明確的焦點指示,但滑鼠點選不會出現(focus-visible 只在鍵盤 focus 時觸發)。
表單與錯誤訊息
表單是 a11y bug 的最大宗。三條規則:第一,每個 <input> 都要有關聯的 <label>;第二,錯誤訊息要用 aria-describedby 綁到 input;第三,錯誤發生時 input 要設 aria-invalid="true"。Day 32 寫的 Input 元件已經有完整實作,這裡把使用方式展示一下:
// app/bookings/new/page.tsx(節錄:表單欄位的 a11y 寫法)
"use client";
import { useState } from "react";
import { Input } from "@/components/ui/Input";
import { Button } from "@/components/ui/Button";
export default function NewBookingForm() {
const [errors, setErrors] = useState&amp;lt;Record&amp;lt;string, string&amp;gt;&amp;gt;({});
return (
&amp;lt;form aria-describedby="form-help" noValidate&amp;gt;
&amp;lt;p id="form-help" className="text-sm text-text-secondary"&amp;gt;
所有欄位皆為必填,Email 格式不正確會跳出提示。
&amp;lt;/p&amp;gt;
&amp;lt;Input
label="姓名"
name="name"
required
hint="請填寫您的真實姓名"
error={errors.name}
autoComplete="name"
/&amp;gt;
&amp;lt;Input
label="Email"
name="email"
type="email"
required
hint="預約確認信會寄到此信箱"
error={errors.email}
autoComplete="email"
/&amp;gt;
&amp;lt;Input
label="電話"
name="phone"
type="tel"
required
hint="格式:0912345678"
error={errors.phone}
autoComplete="tel"
/&amp;gt;
&amp;lt;Button type="submit" loading={false}&amp;gt;確認預約&amp;lt;/Button&amp;gt;
&amp;lt;/form&amp;gt;
);
}
這個表單有三個 a11y 重點。第一,noValidate 關閉瀏覽器原生的 HTML5 validation,因為原生的提示氣泡對螢幕閱讀器不友善、也無法客製化。我們用 React state 自己管錯誤,給的錯誤訊息可以完整朗讀。第二,aria-describedby="form-help" 指向表單上方的說明文字,讓螢幕閱讀器在進入第一個欄位時自動朗讀。第三,每個 input 都用 label 屬性(透過 Day 32 的 Input 元件內部用 htmlFor 綁到 input id),點 label 會 focus 到 input,螢幕閱讀器會唸出 label 文字。
鍵盤導航
鍵盤導航是「可操作性」最常被忽略的細節。預設的瀏覽器行為已經處理了大部分:Tab 移到下一個 focusable 元素、Shift+Tab 反向、Enter 觸發按鈕、Space 切換 checkbox。但有些情境需要手動處理:自訂的下拉選單(<div role="listbox">)需要用上下鍵選擇選項、modal 開啟時要把 focus 移到 modal 內、關閉後還給原本的按鈕。
看一個 modal 的鍵盤處理範例:
// components/ui/Dialog.tsx
"use client";
import { useEffect, useRef, type ReactNode } from "react";
import { createPortal } from "react-dom";
export function Dialog({
open,
onClose,
title,
children,
}: {
open: boolean;
onClose: () =&amp;gt; void;
title: string;
children: ReactNode;
}) {
const ref = useRef&amp;lt;HTMLDivElement&amp;gt;(null);
const previousFocus = useRef&amp;lt;HTMLElement | null&amp;gt;(null);
useEffect(() =&amp;gt; {
if (!open) return;
// 記住原本 focus 的元素,關閉時還回去
previousFocus.current = document.activeElement as HTMLElement;
// 把 focus 移到 dialog 內第一個 focusable 元素
const firstFocusable = ref.current?.querySelector&amp;lt;HTMLElement&amp;gt;(
"button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])",
);
firstFocusable?.focus();
// Esc 關閉
const onKeyDown = (e: KeyboardEvent) =&amp;gt; {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", onKeyDown);
return () =&amp;gt; {
document.removeEventListener("keydown", onKeyDown);
previousFocus.current?.focus();
};
}, [open, onClose]);
if (!open || typeof document === "undefined") return null;
return createPortal(
&amp;lt;div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
&amp;gt;
&amp;lt;div ref={ref} className="rounded-card bg-white p-6 shadow-lg"&amp;gt;
&amp;lt;h2 id="dialog-title" className="text-lg font-semibold"&amp;gt;{title}&amp;lt;/h2&amp;gt;
&amp;lt;div&amp;gt;{children}&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;,
document.body,
);
}
這個 Dialog 有四個關鍵的鍵盤行為。第一,role="dialog" 與 aria-modal="true" 告訴螢幕閱讀器「這是 modal、背景的內容暫時不要朗讀」。第二,aria-labelledby="dialog-title" 指向標題,螢幕閱讀器會唸「對話框:標題文字」。第三,開啟時把 focus 移到第一個 focusable 元素,關閉時還給原本的元素,避免「按下 Esc 之後完全不知道 focus 在哪」。第四,Esc 鍵關閉是業界慣例,視障使用者會直覺期待這個行為。
用 axe-core + Playwright 自動掃 a11y
手動檢查 a11y 容易漏,業界共識是用工具自動掃。axe-core 是規則引擎,@axe-core/playwright 是 Playwright 整合套件。裝好之後寫一支測試自動掃所有頁面:
pnpm add -D @axe-core/playwright@^4.10
// tests/e2e/a11y.spec.ts
// 用 axe-core 自動掃所有公開頁面的 a11y 違規
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
const pages = ["/", "/services", "/login", "/register"];
for (const path of pages) {
test(`a11y: ${path} 通過 axe 掃描`, async ({ page }) =&amp;gt; {
await page.goto(path);
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag22aa"])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
}
這支測試在 CI 跑時如果掃到違規就會失敗。常見的違規:圖片缺 alt、label 沒綁到 input、色彩對比不足、heading 層級跳級(h1 直接到 h3)。每個違規 axe-core 都會給具體的元素 selector 與修正建議,照著修即可。
把這支測試加入 CI workflow(與 Day 27 的 E2E 整合在同一個檔):
# .github/workflows/e2e.yml(追加 a11y job)
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
# ...(與 Day 27 相同)
- name: 跑 E2E + a11y 測試
run: pnpm exec playwright test
對中型專案來說,這層 axe-core 掃描能擋下 70% 的 a11y bug。剩下的 30%(例如「這個按鈕的 aria-label 文案對視障使用者不夠直覺」)需要人工用螢幕閱讀器實測,這層我們在 Day 42 的無障礙總檢會再展開。對個人開發者來說,把 axe-core 整合進 Playwright 與 ESLint 是「CP 值最高」的 a11y 投資——一次設定、永久擋下大部分結構性 bug。
色彩對比與 reduced-motion
色彩對比是 Perceivable 原則最容易被忽略的細節。WCAG 2.2 AA 級要求「一般文字對比至少 4.5:1,大字(18pt 或 14pt 粗體)至少 3:1」。Tailwind 預設的色階大多通過這個標準,但跨色階組合時要特別小心。我們 Day 32 設定的設計 token(--color-text-secondary: #475569)在 --color-surface: #ffffff 上的對比是 7.4:1,AA 級輕鬆通過。
實務上可以用 WebAIM 的 Contrast Checker 工具算出實際數值。Node 環境可以用 color-contrast-checker 套件直接驗證:
pnpm add -D color-contrast-checker
// tests/a11y/contrast.spec.ts
// 自動驗證設計 token 的色彩對比符合 WCAG AA 標準
import { test, expect } from "@playwright/test";
type Pair = { fg: string; bg: string; label: string };
const pairs: Pair[] = [
{ fg: "#0f172a", bg: "#ffffff", label: "text-primary on surface" },
{ fg: "#475569", bg: "#ffffff", label: "text-secondary on surface" },
{ fg: "#4f46e5", bg: "#ffffff", label: "brand-600 on surface (CTA)" },
{ fg: "#ffffff", bg: "#4f46e5", label: "white on brand-600 (button text)" },
{ fg: "#e11d48", bg: "#ffffff", label: "danger-600 on surface (error)" },
];
for (const pair of pairs) {
test(`contrast: ${pair.label} ≥ 4.5:1`, () => {
// 在瀏覽器內用 canvas 算 contrast ratio
// 這裡只示意,實際會引入 color-contrast-checker 工具函式
expect(ratio(pair.fg, pair.bg)).toBeGreaterThanOrEqual(4.5);
});
}
function ratio(fg: string, bg: string): number {
// 簡化:實際會呼叫 color-contrast-checker 的 ratio()
// 為了示意,這裡直接回傳一個常數,實務上請呼叫工具函式
return 7.4;
}
另一個常被忽略的是 prefers-reduced-motion。前庭功能障礙或偏頭痛的使用者會被過度的動畫觸發症狀。CSS 的 @media (prefers-reduced-motion: reduce) 可以讓動畫時間歸零、移除 transition:
/* styles/a11y.css
遵循使用者作業系統的「減少動畫」偏好 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Tailwind 4.x 提供 motion-safe: 與 motion-reduce: 兩個 variant,直接寫在 className 裡就能依使用者偏好切換:
// 使用 motion-safe 與 motion-reduce 區分動畫行為
export function Toast({ message }: { message: string }) {
return (
<div
className={cn(
"rounded-card bg-white p-4 shadow-lg",
// 一般使用者:滑入動畫 200ms
"motion-safe:animate-slide-in motion-safe:duration-200",
// 偏好減少動畫的使用者:直接顯示、無動畫
"motion-reduce:animate-none",
)}
role="status"
>
{message}
</div>
);
}
這個 Toast 同時符合三個 a11y 原則:role="status" 讓螢幕閱讀器主動朗讀、motion-safe / motion-reduce 尊重使用者動畫偏好、文字與背景對比通過 4.5:1。
常見錯誤與踩雷
第一個踩雷是「div onClick 當按鈕」。這是 React 最常見的 a11y bug:用 <div onClick={...}> 看起來可以點,但螢幕閱讀器不會識別為按鈕、鍵盤 Tab 也不會 focus。修法:改用 <button>,或加 role="button"、tabIndex={0}、onKeyDown 處理 Enter 鍵。實務上 99% 的情境應該直接用 <button>。
第二個是「placeholder 當 label」。placeholder 顏色較淺、會在使用者輸入後消失、螢幕閱讀器不一定會朗讀。修法:每個 input 配一個 <label> 元素。如果真的沒有視覺空間放 label(例如登入頁的 Email/密碼),用 aria-label 給螢幕閱讀器一個可讀的名字。
第三個是「顏色單獨傳達資訊」。例如「紅字表示錯誤、綠字表示成功」,色盲使用者看不出來。修法:除了顏色,也用 icon 或文字(aria-label="錯誤")。例如表單錯誤除了紅色邊框,還要在 input 旁邊放一個 ! 圖示並設 aria-label="輸入錯誤"。
第四個是「modal 開啟沒管理 focus」。開 modal 後 focus 還停在背景的按鈕,螢幕閱讀器會繼續朗讀背景。修法:開啟時把 focus 移進 modal、關閉時還回去。上面的 Dialog 元件已經展示完整寫法。
第五個是「dynamic content 沒宣告」。用 React state 更新了一段重要訊息(例如「已成功送出」),但螢幕閱讀器不會自動朗讀新內容。修法:給容器 role="status" 或 aria-live="polite",新內容出現時螢幕閱讀器會主動朗讀。
第六個是「disable 按鈕而不給替代路徑」。例如「送出」按鈕 disable 等使用者填完所有欄位,但螢幕閱讀器使用者不知道為什麼按鈕不能按。修法:不要 disable,按下去的時候用 client-side validation 提示錯誤;或者 disable 同時用 aria-describedby 指向「請先填完所有欄位」的說明。
效能與實務提醒
a11y 工具整合的成本其實不高。@axe-core/playwright 一行就能裝,一支 spec 就能掃所有頁面。建議的時機是「CI 跑 E2E 測試時一起跑」,不要另外開 job——a11y 掃描本身只要幾秒,併在 E2E job 裡跑最划算。
另一個實務提醒是「a11y 不是驗收階段才做的事」。把 a11y 拉到開發階段:每寫一個元件就用 axe DevTools 瀏覽器擴充掃一次,發現問題當下修,等到最後一週才修會累積大量技術債。我們貫穿專案的開發流程(Day 31-44)會把 axe-core 整合進 ESLint 與 Playwright,讓 a11y 違規在 PR 階段就擋下,避免「最後一週才發現 50 個違規要修」的窘境。
最後一個提醒是「自動掃描的盲點」。axe-core 能抓出大部分結構性問題(缺 alt、缺 label、對比不足),但抓不出語意性問題(按鈕的 aria-label 文案不通順、表單流程對認知障礙使用者太複雜)。這層需要人工用 NVDA(Windows)、VoiceOver(macOS)、TalkBack(Android)實測。對 side project 來說,至少要在主要流程(建立預約、登入、預約確認)用螢幕閱讀器走過一輪,確認沒有「聽不懂的提示」、「不知道下一步該做什麼」的卡點。這層在 Day 42 的無障礙總檢會深入展開。
Skip Link 與 landmark
對使用螢幕閱讀器或只能靠鍵盤操作的使用者來說,能「快速跳過導航列直達主要內容」是非常重要的設計。Skip Link(跳轉連結)是業界慣例:頁面最頂端放一個視覺隱藏但螢幕閱讀器可達的連結,點下去直接跳到主要內容。
// components/a11y/SkipLink.tsx
"use client";
import { cn } from "@/lib/cn";
export function SkipLink() {
return (
<a
href="#main-content"
className={cn(
// 預設視覺隱藏
"sr-only",
// focus 時顯示在左上角
"focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50",
"focus:rounded-button focus:bg-brand-600 focus:px-4 focus:py-2 focus:text-white",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
)}
>
跳至主要內容
</a>
);
}
配合 Skip Link,<main> 元素要設 id="main-content" 與 tabIndex={-1}(讓它可以被 focus)。這個寫法是 WAI-ARIA Authoring Practices 推薦的標準 pattern,螢幕閱讀器會自動識別 landmark(<nav>、<main>、<aside>、<footer> 等),讓使用者可以在主要區塊間快速跳轉。
小結
今天把無障礙(a11y)基礎走完一輪。重點回顧:WCAG 2.2 的四原則 POUR 是最高抽象;8 條常見檢查項涵蓋 80% 的實際 bug;semantic HTML + ARIA 是寫對的關鍵;表單的三條規則(label 綁定、錯誤訊息 aria-describedby、aria-invalid)是 a11y bug 最大宗的解法;鍵盤導航在 modal 與自訂元件要手動處理;axe-core + Playwright 能自動掃 70% 的問題。明天 Day 29 會進入部署篇:怎麼把 Next.js 15 專案部署到 Vercel 與自架伺服器,包含環境變數管理、build 設定、CI/CD、CDN、快取策略等主題。
結語
今天的重點是讓 a11y 從「設計師的事」變成「前端工程師的基本功」。透過 semantic HTML、ARIA attributes、鍵盤導航、axe-core 自動化掃描這四條武器,你應該能在寫元件時就考量 a11y、能在 CI 自動擋下大部分問題、能用螢幕閱讀器實測確認主要流程可用。讀完這篇你應該能回答:WCAG 2.2 的四原則是什麼?為什麼 placeholder 不能當 label?怎麼讓 modal 的鍵盤行為正確?怎麼用 axe-core + Playwright 自動掃 a11y?
明天,我們會進入部署篇:怎麼把 Next.js 15 專案部署到 Vercel(最快、最簡單)與自架伺服器(最彈性、最低成本)。會講到環境變數管理、build 設定、CI/CD、CDN、快取策略等主題,並用貫穿專案「預約管理系統」當範例,把 Day 32 寫好的 mock 模式正式切換到 live API。實作上需要一個 Vercel 帳號或一台自己的 Linux 伺服器。
延伸資源
- WCAG 2.2 官方規格:
https://www.w3.org/TR/WCAG22/,四原則、guideline、test criteria 完整目錄。 - axe-core 規則集:
https://github.com/dequelabs/axe-core,所有規則的說明與修正建議。 - @axe-core/playwright:
https://github.com/component-driven/axe-playwright,Playwright 整合與 API。 - MDN — ARIA:
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA,每個 ARIA 屬性的詳細說明。 - WebAIM — Contrast Checker:
https://webaim.org/resources/contrastchecker/,色彩對比計算工具。 - The A11Y Project:
https://www.a11yproject.com/,新手友善的 checklist 與 pattern library。
留言
張貼留言