ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Front-End-Checklist 的无障碍通知规则深度解析:ARIA Live Regions、Toast 实现与验证清单

Front-End-Checklist 的无障碍通知规则深度解析:ARIA Live Regions、Toast 实现与验证清单 Front-End-Checklist 的无障碍通知规则深度解析ARIA Live Regions、Toast 实现与验证清单【免费下载链接】Front-End-Checklist The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist本文基于 Front-End-Checklist 仓库中的accessible-notifications规则「Make notifications accessible」完整讲解如何用 ARIA live regions 让 Toast、内联通知与进度反馈被屏幕阅读器正确朗读从rolestatus/rolealert的选择策略、React Toast 组件与 Provider 的完整实现到停留时长规范、CSS 细节与屏幕阅读器验证清单并结合仓库内真实源码apps/web/lib/accessibility/screen-reader.ts等印证落地方式读完即可在任意项目中写出可被辅助技术感知的通知系统。规则概览为什么通知必须可访问该规则定义于 skills/accessible-notifications/references/rule.md对应的机器可读元数据为Priority: high · Difficulty: intermediate · Time: 25 min与内容规则文件 accessible-notifications.mdx 的 frontmatterpriority: high、difficulty: intermediate、estimatedTime: 25一致归类于 accessibility 与 html 两个类目、components子类。规则的核心论点是没有恰当的 ARIA 属性屏幕阅读器用户会漏掉关键通知——表单错误、成功消息、实时更新——从而对页面状态变化一无所知。Toast 与 Alert 的播报机制依赖两类东西ARIA live regions 与恰当的 role。配套的 SKILL.md 将该规则提炼为四条快速参考Quick Reference这也是后续所有代码示例的设计依据使用aria-live区域播报动态内容变化根据紧急程度在polite等待与assertive打断之间选择确保通知停留时间足够被读完提供可见的与可编程的关闭方式其中「Check / Fix」两条给出了可执行的审查标准验证通知使用了 aria-live 区域、恰当的 rolealert 或 status、且停留时间足够被阅读修复方向是「用rolealert或rolestatus、aria-livepolite或assertive与足够显示时长来实现通知」。基础 HTML 实现三种标准结构规则给出的最小可用示例覆盖了三类场景状态通知polite、告警通知assertive、以及供动态注入内容的 live region 容器!-- Status notification (polite) -- div rolestatus aria-livepolite classnotification Your changes have been saved. /div !-- Alert notification (assertive) -- div rolealert aria-liveassertive classnotification notification--error Error: Please fill in all required fields. /div !-- Live region container (content injected dynamically) -- div idnotifications aria-livepolite aria-atomictrue classsr-only /div三个结构各有分工rolestatus隐式带有aria-livepolite适合保存成功、进度等非紧急反馈屏幕阅读器会在当前播报结束后再朗读rolealert隐式带有aria-liveassertive适合表单错误、时间敏感告警会立即打断当前播报空的 live region 容器这是最容易被忽视但最关键的模式——容器必须先于内容变化存在于 DOM 中之后再注入文本才会触发播报。这也是为什么示例中容器是空的并带有id与aria-atomictrue保证内容被整体朗读而非碎片化。仓库中另一条规则 aria-live-regions.mdx 对此有专门的反例/正例对照带内容一起创建的 live region❌不会被播报而「先挂载空区域、后改textContent」✅才会。ARIA Live Region 类型速查属性行为适用场景aria-livepolite等待用户停顿当前播报结束状态更新、非紧急信息aria-liveassertive立即打断错误、时间敏感告警rolestatus隐式 polite进度、成功消息rolealert隐式 assertive错误、警告从源码结构看「role 隐式 live region」意味着在实际项目中两者写其一即可达到同样效果上面 HTML 示例中同时书写role与aria-live是显式冗余便于人工审查时一眼确认语义属于防御性写法。React Toast 组件规则给出了一个可直接用于生产形态的 Toast 组件。关键点在于按类型切换role与aria-live错误/警告 → alert/assertive其余 → status/polite、aria-atomictrue保证整条消息一次性朗读、tabIndex{-1}配合focus()让键盘用户能感知到通知出现、图标aria-hiddentrue避免符号被读出。import { useEffect, useRef } from react type NotificationType success | error | warning | info interface ToastProps { message: string type: NotificationType duration?: number onDismiss: () void } export function Toast({ message, type, duration 5000, onDismiss }: ToastProps) { const toastRef useRefHTMLDivElement(null) // Auto-dismiss after duration useEffect(() { if (duration 0) { const timer setTimeout(onDismiss, duration) return () clearTimeout(timer) } }, [duration, onDismiss]) // Focus toast for keyboard users useEffect(() { toastRef.current?.focus() }, []) const isError type error || type warning return ( div ref{toastRef} role{isError ? alert : status} aria-live{isError ? assertive : polite} aria-atomictrue tabIndex{-1} className{toast toast--${type}} span classNametoast__icon aria-hiddentrue {type success ✓} {type error ✕} {type warning ⚠} {type info ℹ} /span span classNametoast__message{message}/span button typebutton onClick{onDismiss} aria-labelDismiss notification classNametoast__dismiss × /button /div ) }两个值得注意的实现细节duration 5000是默认值duration 0表示永不自动消失if (duration 0)分支保护了这一点与后文「错误消息不自动消失」的时长规范对应关闭按钮必须是有可访问名称的buttonaria-labelDismiss notification而不是无名称的×字符——这满足 Quick Reference 中「提供可见的与可编程的关闭方式」一条。Toast 容器Context、Provider 与双重播报设计完整的 Toast 系统由 Provider 托管状态并采用「视觉容器 隐藏播报区」的双层结构import { createContext, useContext, useState, useCallback } from react interface Notification { id: string message: string type: NotificationType duration?: number } interface ToastContextType { addToast: (notification: OmitNotification, id) void removeToast: (id: string) void } const ToastContext createContextToastContextType | null(null) export function ToastProvider({ children }: { children: React.ReactNode }) { const [toasts, setToasts] useStateNotification[]([]) const addToast useCallback((notification: OmitNotification, id) { const id Math.random().toString(36).substr(2, 9) setToasts(prev [...prev, { ...notification, id }]) }, []) const removeToast useCallback((id: string) { setToasts(prev prev.filter(t t.id ! id)) }, []) return ( ToastContext.Provider value{{ addToast, removeToast }} {children} {/* Toast container with live region */} div classNametoast-container aria-labelNotifications {toasts.map(toast ( Toast key{toast.id} message{toast.message} type{toast.type} duration{toast.duration} onDismiss{() removeToast(toast.id)} / ))} /div {/* Screen reader announcement region */} div rolestatus aria-livepolite aria-atomictrue classNamesr-only {toasts.length 0 toasts[toasts.length - 1].message} /div /ToastContext.Provider ) } export function useToast() { const context useContext(ToastContext) if (!context) throw new Error(useToast must be used within ToastProvider) return context }这里的设计意图可以分三层理解useToast()钩子通过 Context 向任意子组件暴露addToast/removeToast并在脱离 Provider 时显式抛错属于快速失败fail fastaria-labelNotifications的可见容器为整组 Toast 提供了一个可定位的组名sr-only的兜底播报区始终挂载且最后一条消息文本持续更新——这保证了即便单个 Toast 组件因自动消失被移出 DOM屏幕阅读器仍能从稳定存在的 live region 中获得播报呼应「live region 必须先存在」的原则。从源码结构看这是一种双保险单条 Toast 自带 role 与 aria-liveProvider 又维护一个持久区域牺牲少量重复播报换取可靠性。使用示例错误永不自动消失规则给出的SaveButton示例演示了完整的交互闭环其中duration的取值直接体现时长策略function SaveButton() { const { addToast } useToast() const handleSave async () { try { await saveData() addToast({ message: Changes saved successfully, type: success, duration: 3000 }) } catch (error) { addToast({ message: Failed to save changes. Please try again., type: error, duration: 0 // Dont auto-dismiss errors }) } } return button onClick{handleSave}Save/button }成功消息 3 秒后自动消失错误消息duration: 0永不消失必须由用户手动关闭——因为错误信息可能需要用户阅读、采取行动后才能处理。内联通知Inline Notifications除浮层 Toast 外规则还覆盖了表单内嵌式通知结构与 Toast 一致但支持标题与可选关闭interface InlineNotificationProps { type: error | warning | success | info title?: string children: React.ReactNode dismissible?: boolean onDismiss?: () void } export function InlineNotification({ type, title, children, dismissible false, onDismiss }: InlineNotificationProps) { const isUrgent type error || type warning return ( div role{isUrgent ? alert : status} aria-live{isUrgent ? assertive : polite} className{notification notification--${type}} {title ( strong classNamenotification__title{title}/strong )} div classNamenotification__content{children}/div {dismissible ( button typebutton onClick{onDismiss} aria-labelDismiss classNamenotification__dismiss × /button )} /div ) }判断逻辑与 Toast 相同error/warning 归为 urgent但注意一个实践差异内联通知通常不需要主动抢焦点它出现在文档流中视觉位置即语义位置这与浮层 Toast 需要focus()拉回注意力形成对比。进度类通知aria-busy 与视觉/朗读内容分离上传进度是实时更新的典型场景。规则给出的UploadProgress组件展示了三个要点aria-busy标记进行中状态、视觉元素整体aria-hidden、以及一个sr-only的完整语义句子function UploadProgress({ progress, fileName }: { progress: number; fileName: string }) { return ( div rolestatus aria-livepolite aria-busy{progress 100} classNameupload-progress span classNamesr-only Uploading {fileName}: {progress}% complete /span div aria-hiddentrue span{fileName}/span progress value{progress} max100 / span{progress}%/span /div /div ) }要点解析aria-busy{progress 100}在未完成期间让屏幕阅读器知道该区域正在加载避免用户把「内容在变」误解为「出错了」视觉部分文件名字、progress元素、百分比数字被aria-hiddentrue整体屏蔽防止碎片化朗读「upload-report.pdf」「42%」分开读毫无意义sr-only中的「Uploading {fileName}: {progress}% complete」是唯一会被朗读的完整句子。注意这仍是一个高频更新场景实际项目中应配合节流例如每 25% 或每 1 秒播报一次否则polite队列会被百分比数字淹没——这一点可从 aria-live-regions.mdx 的「off用于高频更新内容」一行推断为通用原则。停留时长规范Timing Guidelines通知类型建议停留时长成功消息3–5 秒信息/状态5–7 秒警告8–10 秒或手动关闭错误不自动消失仅手动规则同时给出了可直接使用的常量映射const DURATION_MAP { success: 3000, info: 5000, warning: 8000, error: 0, // No auto-dismiss } as consterror: 0与Toast组件中if (duration 0)的守卫逻辑形成配套0 就是「永不自动关闭」的约定值。这套数值可以直接作为团队设计系统的默认参数。样式实现sr-only 与 prefers-reduced-motion完整样式如下其中.sr-only是整套「视觉隐藏但可朗读」模式的物理基础末尾的prefers-reduced-motion查询体现了动效的无障碍兜底.toast-container { position: fixed; bottom: 1rem; right: 1rem; z-index: 1000; display: flex; flex-direction: column; gap: 0.5rem; } .toast { display: flex; align-items: center; gap: 0.75rem; padding: 1rem; border-radius: 0.5rem; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); animation: slideIn 0.3s ease-out; } .toast--success { background: #d4edda; border-left: 4px solid #28a745; } .toast--error { background: #f8d7da; border-left: 4px solid #dc3545; } .toast--warning { background: #fff3cd; border-left: 4px solid #ffc107; } .toast--info { background: #d1ecf1; border-left: 4px solid #17a2b8; } .toast__dismiss { background: none; border: none; font-size: 1.25rem; cursor: pointer; padding: 0.25rem; margin-left: auto; } /* Screen reader only */ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; } keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } /* Respect motion preferences */ media (prefers-reduced-motion: reduce) { .toast { animation: none; } }两个细节值得强调.sr-only必须是「视觉裁剪」而非display: nonedisplay: none/visibility: hidden的元素不会进入可访问性树live region 也就失效1px clip: rect(0,0,0,0)的方案让元素保持可朗读类型颜色除背景色外都附带 4px 左边框色如 error 的border-left: 4px solid #dc3545保证色盲用户也能仅凭边框区分通知类型——这与仓库中 color-contrast 等规则一脉相承。验证清单Verification规则给出的六步验证流程覆盖了辅助技术、键盘与焦点三个维度启用屏幕阅读器并触发通知确认播报发生在恰当的时间点polite 不抢话、assertive 立即打断测试键盘关闭方式Escape 键检查通知没有消失过快验证关闭后的焦点管理焦点不应掉落到不可预期的位置使用多种屏幕阅读器交叉测试NVDA、VoiceOver、JAWS。自动化工具axe、Lighthouse 等能发现缺失的role/aria-live属性但播报时机、队列顺序与焦点行为必须人工用真实屏幕阅读器验证——这也是该规则 difficulty 为 intermediate 的原因。仓库内的真实落地源码印证Front-End-Checklist 网站自身就在应用这条规则。以下实现可作为「规则如何落到 Next.js 项目」的参考1. 屏幕阅读器播报工具函数—— screen-reader.ts 提供了两种模式与规则中的「临时区域」和「持久区域」一一对应/** Announces a message to screen readers via a temporary ARIA live region. */ export function announce(message: string, priority: polite | assertive polite): void { const announcement document.createElement(div) announcement.setAttribute(role, status) announcement.setAttribute(aria-live, priority) announcement.setAttribute(aria-atomic, true) announcement.className sr-only announcement.textContent message document.body.appendChild(announcement) setTimeout(() { document.body.removeChild(announcement) }, 1000) } /** Creates a persistent ARIA live region for repeated announcements; returns announce/destroy handles. */ export function createLiveRegion(priority: polite | assertive polite): { announce: (message: string) void destroy: () void } { const region document.createElement(div) region.setAttribute(role, status) region.setAttribute(aria-live, priority) region.setAttribute(aria-atomic, true) region.className sr-only document.body.appendChild(region) return { announce: (message: string) { region.textContent void region.offsetHeight region.textContent message }, destroy: () { document.body.removeChild(region) } } }值得注意的是createLiveRegion().announce中的两行「清空 → 强制 reflowvoid region.offsetHeight→ 再写入」序列可以推断其目的是规避部分屏幕阅读器对「连续写入相同文本不触发播报」的已知问题即通过清空建立一次真实变化。2. 错误边界默认用rolealert—— error-boundary.tsx 的默认 fallback 是div rolealert隐含 assertive而SectionErrorFallback使用rolealert aria-livepolite的组合——渲染失败属于错误级别但团队选择了 polite 节奏以减少打断这是一个「按产品语境调节 politeness」的实例。3. 页面级错误页的分级策略—— 路由级错误页 error.tsx/error.tsx) 使用rolealert aria-livepolite而整站崩溃的 global-error.tsx 升级为rolealert aria-liveassertive影响范围越大打断级别越高与规则中「assertive 仅用于真正紧急的消息」一致。4. 表单提交错误用rolealert—— waitlist-form.tsx 与 cli-notify-form.tsx 都在submitError出现时渲染带rolealert的span/p对应规则中「表单错误属于关键通知」的结论。5. 非紧急状态优先原生语义—— checklist-browser.tsx 用output aria-livepolite播报筛选结果数量rule-checkbox.tsx 用aria-livepolite的span播报「Saving...」。output本身就是隐式 live region 的原生元素符合 aria-live-regions.mdx Exceptions 中「优先原生 HTML 语义而非 ARIA」的原则。常见陷阱不要滥用 assertive规则末尾的警告值得原样记住把aria-liveassertive留给真正紧急的消息。过度使用会不断打断屏幕阅读器的输出破坏用户体验。结合前文的判断矩阵实际编码时可以用一条简单决策链是错误/警告吗 → 是则 alert/assertive是进度/成功/状态吗 → status/polite高频更新吗 → 考虑off或节流。仓库中 global-errorassertive与 SectionErrorFallbackpolite的分级差异正是这条决策链的现实样本。相关规则与延伸阅读aria-live-regions.mdxlive region 基础规则包含 politeness 级别表、off的用途、以及「live region 必须先于内容存在于 DOM」的正反例accessible-notifications.mdx本规则的完整 frontmatter 版本含来源引用MDN HTML、WHATWG HTML Living Standard与相关规则列表SKILL.md面向 AI Agent 的精简版规则说明Quick Reference / Check / Fix / Explain / Code Review 五段结构同属html/components子类、常与本规则一起审查的相邻规则carousel-accessibility、accessible-tooltips、custom-element-accessibility。适用前提本文代码示例为框架无关的 React 18 语义模式规则本身role / aria-live / 停留时长 / 验证清单对任意技术栈的模板、服务端渲染 HTML 与共享组件均适用SKILL.md 中明确指出应审查「最终面向浏览器的标记而非仅源框架的抽象」即 SSR 场景下要检查渲染产物。【免费下载链接】Front-End-Checklist The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表