跳转到主要内容

可访问性

可访问性不是“加 aria 就行”,而是语义、键盘、焦点与反馈闭环共同决定的体验质量。

从语义开始

可访问性最省心的方式是把“基础但无聊”的事做对:正确的 HTML 元素、label 与关联关系。ARIA 是补充,不是地基。

  • 动作用 <button>,导航用 <a href>。不要用 div + onClick 冒充按钮。
  • 每个输入都需要 label(可见或 aria-label),并且 name/id 稳定。
  • 如果你加了 ARIA,确保 role/state 正确,并且与 UI 同步更新。

键盘交互模式

把常见组件的键盘行为标准化,是最快“把可访问性做对”的方式。

  • 菜单/列表:方向键移动焦点;Enter 选择;Esc 关闭。
  • 对话框:打开后焦点进入对话框;关闭后回到触发按钮。
  • Tabs:方向键切换 tab;激活的 tab 控制面板显示。

焦点管理

  • 焦点必须可见:不要移除 outline;如果替换,也要可见且对比足够。
  • 路由切换后,在合适场景把焦点移动到新页面标题。
  • 表单提交失败时,聚焦第一个错误字段,并宣读错误汇总。

让状态可感知(aria-live、role)

辅助技术用户需要“变化反馈”:加载、错误、保存成功等都要能被感知。

TypeScript
export function SaveStatus({ status }: { status: 'idle' | 'saving' | 'saved' | 'error' }) {
  return (
    <div aria-live="polite">
      {status === 'saving' ? 'Saving…' : null}
      {status === 'saved' ? 'Saved.' : null}
      {status === 'error' ? <span role="alert">Save failed.</span> : null}
    </div>
  );
}

示例:最小可访问对话框

对话框至少要有:正确的 role、aria-modal、可标注的标题、打开时聚焦、关闭时焦点回收。

TypeScript
import { useEffect, useId, useRef } from 'react';

export function Dialog({
  open,
  title,
  onClose,
  children,
}: {
  open: boolean;
  title: string;
  onClose: () => void;
  children: React.ReactNode;
}) {
  const titleId = useId();
  const panelRef = useRef<HTMLDivElement | null>(null);
  const triggerRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (!open) return;
    triggerRef.current = document.activeElement as HTMLElement | null;
    panelRef.current?.focus();
  }, [open]);

  useEffect(() => {
    if (open) return;
    triggerRef.current?.focus();
  }, [open]);

  if (!open) return null;

  return (
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby={titleId}
      className="fixed inset-0 bg-black/50 flex items-center justify-center p-4"
      onKeyDown={(e) => {
        if (e.key === 'Escape') onClose();
      }}
    >
      <div
        ref={panelRef}
        tabIndex={-1}
        className="bg-white rounded p-4 w-full max-w-md dark:bg-gray-800"
      >
        <div className="flex items-center justify-between gap-3">
          <h2 id={titleId} className="font-bold">
            {title}
          </h2>
          <button type="button" onClick={onClose} aria-label="Close">
            ×
          </button>
        </div>
        <div className="mt-3">{children}</div>
      </div>
    </div>
  );
}

检查清单

  • 所有可交互元素都能通过 Tab 到达。
  • 焦点可见,关闭弹窗后能回到合理位置。
  • 表单错误可被宣读(role=alert)并与输入关联(aria-describedby)。
  • 没有键盘陷阱:Esc 可关闭弹窗,焦点能回到合理位置。
  • 交互控件都有可访问名称(label/aria-label)。

延伸阅读

可访问性 - Guides - React 文档 - React 文档