跳到主要內容

FE Day 28 無障礙(a11y)基礎

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:

  1. Perceivable - 非文字內容有替代文字:<img> 一定要有 alt;裝飾性圖片用 alt="";icon 按鈕用 aria-label。
  2. Perceivable - 色彩對比至少 4.5:1:一般文字對背景至少 4.5:1、大字(18pt 以上)至少 3:1。Tailwind 的 slate-600 在 white 上是 5.7:1 通過。
  3. Operable - 所有功能可鍵盤操作:不用滑鼠也能完成所有操作。div onClick 不行,要用 <button>。
  4. Operable - 焦點可見:鍵盤 focus 必須有視覺指示。Tailwind 的 focus-visible:ring-2 是標準做法。
  5. Understandable - 表單欄位有 label:<input> 一定要有關聯的 <label>;placeholder 不是 label。
  6. Understandable - 語言有宣告:<html lang="zh-Hant">;切換語言的段落用 lang="en"。
  7. Robust - HTML 結構合法:標籤正確嵌套、屬性值正確,例如 <a href> 不能只放 <div> 充當連結。
  8. 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;amp;lt;HTMLButtonElement&amp;amp;gt; &amp;amp;amp; {
  variant?: Variant;
  size?: Size;
  loading?: boolean;
  loadingLabel?: string;
  children: ReactNode;
};

const variantClasses: Record&amp;amp;lt;Variant, string&amp;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;amp;lt;Size, string&amp;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;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;amp;gt;
      {loading ? (
        &amp;amp;lt;&amp;amp;gt;
          &amp;amp;lt;span aria-hidden="true"&amp;amp;gt;...&amp;amp;lt;/span&amp;amp;gt;
          &amp;amp;lt;span className="sr-only"&amp;amp;gt;{loadingLabel}&amp;amp;lt;/span&amp;amp;gt;
        &amp;amp;lt;/&amp;amp;gt;
      ) : (
        children
      )}
    &amp;amp;lt;/button&amp;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;amp;lt;Record&amp;amp;lt;string, string&amp;amp;gt;&amp;amp;gt;({});

  return (
    &amp;amp;lt;form aria-describedby="form-help" noValidate&amp;amp;gt;
      &amp;amp;lt;p id="form-help" className="text-sm text-text-secondary"&amp;amp;gt;
        所有欄位皆為必填,Email 格式不正確會跳出提示。
      &amp;amp;lt;/p&amp;amp;gt;

      &amp;amp;lt;Input
        label="姓名"
        name="name"
        required
        hint="請填寫您的真實姓名"
        error={errors.name}
        autoComplete="name"
      /&amp;amp;gt;
      &amp;amp;lt;Input
        label="Email"
        name="email"
        type="email"
        required
        hint="預約確認信會寄到此信箱"
        error={errors.email}
        autoComplete="email"
      /&amp;amp;gt;
      &amp;amp;lt;Input
        label="電話"
        name="phone"
        type="tel"
        required
        hint="格式:0912345678"
        error={errors.phone}
        autoComplete="tel"
      /&amp;amp;gt;

      &amp;amp;lt;Button type="submit" loading={false}&amp;amp;gt;確認預約&amp;amp;lt;/Button&amp;amp;gt;
    &amp;amp;lt;/form&amp;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;amp;gt; void;
  title: string;
  children: ReactNode;
}) {
  const ref = useRef&amp;amp;lt;HTMLDivElement&amp;amp;gt;(null);
  const previousFocus = useRef&amp;amp;lt;HTMLElement | null&amp;amp;gt;(null);

  useEffect(() =&amp;amp;gt; {
    if (!open) return;
    // 記住原本 focus 的元素,關閉時還回去
    previousFocus.current = document.activeElement as HTMLElement;

    // 把 focus 移到 dialog 內第一個 focusable 元素
    const firstFocusable = ref.current?.querySelector&amp;amp;lt;HTMLElement&amp;amp;gt;(
      "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])",
    );
    firstFocusable?.focus();

    // Esc 關閉
    const onKeyDown = (e: KeyboardEvent) =&amp;amp;gt; {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", onKeyDown);

    return () =&amp;amp;gt; {
      document.removeEventListener("keydown", onKeyDown);
      previousFocus.current?.focus();
    };
  }, [open, onClose]);

  if (!open || typeof document === "undefined") return null;

  return createPortal(
    &amp;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;amp;gt;
      &amp;amp;lt;div ref={ref} className="rounded-card bg-white p-6 shadow-lg"&amp;amp;gt;
        &amp;amp;lt;h2 id="dialog-title" className="text-lg font-semibold"&amp;amp;gt;{title}&amp;amp;lt;/h2&amp;amp;gt;
        &amp;amp;lt;div&amp;amp;gt;{children}&amp;amp;lt;/div&amp;amp;gt;
      &amp;amp;lt;/div&amp;amp;gt;
    &amp;amp;lt;/div&amp;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;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`, () =&gt; {
    // 在瀏覽器內用 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 (
    &lt;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"
    &gt;
      {message}
    &lt;/div&gt;
  );
}

這個 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。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...