2026年3月25日
使用 Hooks 建構無障礙 React 元件
無障礙不是上線前才需要檢查的清單,而是從第一行程式碼開始就需要貫徹的設計約束。談到 React 中的無障礙,大多數開發者會想到 ARIA 屬性、語義化 HTML 和螢幕閱讀器支援。這些確實重要。但還有一個完整的無障礙類別很少受到關注:尊重使用者在作業系統層級已經設定好的偏好。
每個主流作業系統都允許使用者設定減少動畫、高對比度、深色模式和文字方向等偏好。這些不是裝飾性的選擇。啟用「減少動畫」的使用者可能患有前庭功能障礙,動畫過渡會讓他們感到身體不適。啟用高對比度的使用者可能視力不佳。當你的 React 應用程式忽略這些訊號時,這不僅僅是功能缺失——而是一道屏障。
本文將向你展示如何使用 ReactUse 的 hooks 在 React 中偵測和回應這些作業系統層級的偏好。我們將涵蓋減少動畫、對比度偏好、色彩配置偵測、焦點管理和文字方向——然後將所有內容整合到一個實際的元件中。
手動監聽媒體查詢的問題
瀏覽器透過 CSS 媒體查詢(如 prefers-reduced-motion、prefers-contrast 和 prefers-color-scheme)暴露作業系統層級的偏好。你可以在 JavaScript 中使用 window.matchMedia 來讀取這些值。手動實作的方式如下:
import { useState, useEffect } from "react";
function useManualReducedMotion(): boolean {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setPrefersReducedMotion(mediaQuery.matches);
const handler = (event: MediaQueryListEvent) => {
setPrefersReducedMotion(event.matches);
};
mediaQuery.addEventListener("change", handler);
return () => mediaQuery.removeEventListener("change", handler);
}, []);
return prefersReducedMotion;
}
這段程式碼能運作,但存在問題。你需要處理 SSR(window 不存在的情況)、管理事件監聽器的清理,並且需要為每個想要追蹤的媒體查詢重複這個模式。將這個模式乘以減少動畫、對比度、色彩配置和其他查詢,你最終會得到大量容易出錯的樣板程式碼。
ReactUse 提供的 hooks 封裝了這個模式,包含正確的 SSR 處理、適當的清理邏輯,以及當使用者更改系統偏好時的即時更新。
useReducedMotion:尊重動畫偏好
useReducedMotion hook 偵測使用者是否在裝置上啟用了「減少動畫」設定。這是你能使用的最具影響力的無障礙 hooks 之一,因為動畫可能會給前庭功能障礙的使用者帶來實際的身體不適。
import { useReducedMotion } from "@reactuses/core";
function AnimatedCard({ children }: { children: React.ReactNode }) {
const prefersReducedMotion = useReducedMotion();
return (
<div
style={{
transition: prefersReducedMotion
? "none"
: "transform 0.3s ease, opacity 0.3s ease",
animation: prefersReducedMotion ? "none" : "fadeIn 0.5s ease-in",
}}
>
{children}
</div>
);
}
這裡的關鍵不是簡單地停用動畫——而是在沒有動畫的情況下提供等價的體驗。對於大多數使用者需要 500ms 淡入的卡片,對於偏好減少動畫的使用者應該立即顯示。內容相同,只是呈現方式不同。
你還可以使用這個 hook 在不同的動畫策略之間切換:
import { useReducedMotion } from "@reactuses/core";
function PageTransition({ children }: { children: React.ReactNode }) {
const prefersReducedMotion = useReducedMotion();
if (prefersReducedMotion) {
// 即時過渡——沒有動畫,但仍然有視覺變化
return <div style={{ opacity: 1 }}>{children}</div>;
}
// 為未選擇減少動畫的使用者提供完整的滑入動畫
return (
<div
style={{
animation: "slideInFromRight 0.4s ease-out",
}}
>
{children}
</div>
);
}
usePreferredContrast:適應對比度需求
usePreferredContrast hook 讀取 prefers-contrast 媒體查詢,告訴你使用者想要更多對比度、更少對比度,還是沒有偏好。這對視力不佳的使用者至關重要。
import { usePreferredContrast } from "@reactuses/core";
function ThemedButton({ children, onClick }: {
children: React.ReactNode;
onClick: () => void;
}) {
const contrast = usePreferredContrast();
const getButtonStyles = () => {
switch (contrast) {
case "more":
return {
backgroundColor: "#000000",
color: "#FFFFFF",
border: "3px solid #FFFFFF",
fontWeight: 700 as const,
};
case "less":
return {
backgroundColor: "#E8E8E8",
color: "#333333",
border: "1px solid #CCCCCC",
fontWeight: 400 as const,
};
default:
return {
backgroundColor: "#3B82F6",
color: "#FFFFFF",
border: "2px solid transparent",
fontWeight: 500 as const,
};
}
};
return (
<button onClick={onClick} style={getButtonStyles()}>
{children}
</button>
);
}
當使用者要求更高對比度時,你應該增大前景和背景顏色之間的差異、使用更粗的字型粗細、讓邊框更明顯。當他們要求更低對比度時,柔化視覺強度。預設分支處理未設定偏好的使用者。
usePreferredColorScheme:系統主題偵測
usePreferredColorScheme hook 告訴你使用者的作業系統是設定為淺色模式、深色模式,還是沒有偏好。這是建構主題感知元件的基礎。
import { usePreferredColorScheme } from "@reactuses/core";
function AdaptiveCard({ title, body }: { title: string; body: string }) {
const colorScheme = usePreferredColorScheme();
const isDark = colorScheme === "dark";
return (
<div
style={{
backgroundColor: isDark ? "#1E293B" : "#FFFFFF",
color: isDark ? "#E2E8F0" : "#1E293B",
border: `1px solid ${isDark ? "#334155" : "#E2E8F0"}`,
borderRadius: "8px",
padding: "24px",
}}
>
<h3 style={{ marginTop: 0 }}>{title}</h3>
<p>{body}</p>
</div>
);
}
如果你只需要一個簡單的布林值判斷,ReactUse 還提供了 usePreferredDark,當使用者偏好深色配置時回傳 true。如果你需要一個完整的深色模式切換並持久化使用者的選擇,useDarkMode 可以開箱即用。
對於更細粒度的媒體查詢控制,useMediaQuery 讓你訂閱任何 CSS 媒體查詢字串並獲得即時更新。
useFocus:鍵盤導覽和焦點管理
鍵盤導覽是核心無障礙要求。無法使用滑鼠的使用者依賴 Tab 鍵在互動元素之間移動。useFocus hook 提供了對焦點的程式化控制,這對於模態對話框、下拉式選單和動態內容至關重要。
import { useRef } from "react";
import { useFocus } from "@reactuses/core";
function SearchBar() {
const inputRef = useRef<HTMLInputElement>(null);
const [focused, setFocused] = useFocus(inputRef);
return (
<div>
<input
ref={inputRef}
type="search"
placeholder="Search..."
style={{
outline: focused ? "2px solid #3B82F6" : "1px solid #D1D5DB",
padding: "8px 12px",
borderRadius: "6px",
width: "100%",
}}
/>
<button onClick={() => setFocused(true)}>
Focus Search (Ctrl+K)
</button>
</div>
);
}
這個 hook 同時回傳當前焦點狀態和一個設定函式。你可以使用焦點狀態來套用視覺指示器(超出瀏覽器預設樣式),並使用設定函式來程式化地移動焦點——例如,當模態框開啟時或當觸發鍵盤快捷鍵時。
將此與 useActiveElement 配合使用,可以追蹤整個應用程式中當前擁有焦點的元素,這對於建構焦點陷阱和跳過導覽連結非常有用。
useTextDirection:RTL 和 LTR 支援
國際化和無障礙有很大的重疊。useTextDirection hook 偵測和管理文件的文字方向,支援從左到右(LTR)和從右到左(RTL)佈局。
import { useTextDirection } from "@reactuses/core";
function NavigationMenu() {
const [dir, setDir] = useTextDirection();
return (
<nav
style={{
display: "flex",
flexDirection: dir === "rtl" ? "row-reverse" : "row",
gap: "16px",
padding: "12px 24px",
}}
>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
<button onClick={() => setDir(dir === "rtl" ? "ltr" : "rtl")}>
Toggle Direction
</button>
</nav>
);
}
RTL 支援影響的不僅僅是文字對齊。導覽順序、圖示位置和 margin/padding 方向都需要翻轉。透過使用 useTextDirection 作為唯一資料來源,你可以建構自動適應的佈局邏輯。
綜合範例:無障礙通知元件
下面是一個將多個無障礙 hooks 整合到單一元件中的實際範例——一個尊重動畫偏好、適應對比度設定、跟隨系統色彩配置並正確管理焦點的通知提示:
import { useRef, useEffect } from "react";
import {
useReducedMotion,
usePreferredContrast,
usePreferredColorScheme,
useFocus,
} from "@reactuses/core";
interface NotificationProps {
message: string;
type: "success" | "error" | "info";
visible: boolean;
onDismiss: () => void;
}
function AccessibleNotification({
message,
type,
visible,
onDismiss,
}: NotificationProps) {
const prefersReducedMotion = useReducedMotion();
const contrast = usePreferredContrast();
const colorScheme = usePreferredColorScheme();
const dismissRef = useRef<HTMLButtonElement>(null);
const [, setFocused] = useFocus(dismissRef);
const isDark = colorScheme === "dark";
const isHighContrast = contrast === "more";
// 通知出現時將焦點移至關閉按鈕
useEffect(() => {
if (visible) {
setFocused(true);
}
}, [visible, setFocused]);
if (!visible) return null;
const colors = {
success: {
bg: isDark ? "#064E3B" : "#ECFDF5",
border: isHighContrast ? "#FFFFFF" : isDark ? "#10B981" : "#6EE7B7",
text: isDark ? "#A7F3D0" : "#065F46",
},
error: {
bg: isDark ? "#7F1D1D" : "#FEF2F2",
border: isHighContrast ? "#FFFFFF" : isDark ? "#EF4444" : "#FCA5A5",
text: isDark ? "#FECACA" : "#991B1B",
},
info: {
bg: isDark ? "#1E3A5F" : "#EFF6FF",
border: isHighContrast ? "#FFFFFF" : isDark ? "#3B82F6" : "#93C5FD",
text: isDark ? "#BFDBFE" : "#1E40AF",
},
};
const scheme = colors[type];
return (
<div
role="alert"
aria-live="assertive"
style={{
position: "fixed",
top: "16px",
right: "16px",
backgroundColor: scheme.bg,
color: scheme.text,
border: `${isHighContrast ? "3px" : "1px"} solid ${scheme.border}`,
borderRadius: "8px",
padding: "16px 20px",
maxWidth: "400px",
display: "flex",
alignItems: "center",
gap: "12px",
fontWeight: isHighContrast ? 700 : 400,
// 尊重動畫偏好
animation: prefersReducedMotion ? "none" : "slideIn 0.3s ease-out",
transition: prefersReducedMotion ? "none" : "opacity 0.2s ease",
}}
>
<span style={{ flex: 1 }}>{message}</span>
<button
ref={dismissRef}
onClick={onDismiss}
aria-label="關閉通知"
style={{
background: "none",
border: `1px solid ${scheme.text}`,
color: scheme.text,
cursor: "pointer",
borderRadius: "4px",
padding: "4px 8px",
fontWeight: isHighContrast ? 700 : 500,
}}
>
關閉
</button>
</div>
);
}
這個元件展示了幾個無障礙原則的協同運作:
role="alert"和aria-live="assertive"確保螢幕閱讀器立即播報通知。useReducedMotion為偏好減少動畫的使用者停用滑入動畫。usePreferredContrast為需要更高對比度的使用者增加邊框寬度和字型粗細。usePreferredColorScheme根據使用者的淺色或深色主題適配所有顏色。useFocus將鍵盤焦點移至關閉按鈕,使使用者無需使用滑鼠就能操作通知。
為什麼 Hooks 是無障礙的正確抽象
Hooks 具有可組合性。每個無障礙關注點都封裝在自己的 hook 中,你可以按需組合它們。一個簡單的按鈕可能只使用 usePreferredContrast。一個複雜的模態框可能使用我們介紹的全部五個 hooks。這些 hooks 互相獨立,這意味著你可以逐步採用它們,無需重構現有程式碼。
Hooks 還能即時回應變化。如果使用者在你的應用程式開啟時從淺色切換到深色模式,hooks 會更新,你的元件會使用新的偏好重新渲染。這是僅使用 CSS 的方案(依賴靜態類別名稱)難以實現的。
安裝
透過套件管理器安裝 ReactUse:
npm install @reactuses/core
然後匯入你需要的 hooks:
import {
useReducedMotion,
usePreferredContrast,
usePreferredColorScheme,
useFocus,
useTextDirection,
} from "@reactuses/core";
相關 Hooks
useReducedMotion— 偵測prefers-reduced-motion偏好usePreferredContrast— 偵測prefers-contrast偏好usePreferredColorScheme— 偵測prefers-color-scheme(淺色、深色或無偏好)usePreferredDark— 深色模式偵測的布林值簡寫useDarkMode— 帶持久化的完整深色模式切換useMediaQuery— 訂閱任何 CSS 媒體查詢useFocus— 程式化焦點管理useActiveElement— 追蹤當前擁有焦點的元素useTextDirection— 偵測和控制 LTR/RTL 文字方向
ReactUse 提供了 100 多個 React hooks。探索全部 →