在现代 Web 应用中,Toast 通知组件是提供用户反馈的标准方式,如操作成功、错误提示或加载状态。然而,传统实现往往忽略无障碍性,导致屏幕阅读器用户错过关键信息。本文聚焦单一技术点:用纯原生 JS/CSS 构建符合 WCAG AA 级的可访问 Toast,核心通过 ARIA live regions 实现实时播报、CSS transitions 处理 enter/exit 与栈叠动画、JS 管理键盘焦点陷阱与智能超时。通过这些参数化工程实践,确保视觉用户与辅助技术用户获得一致体验。
ARIA Live Regions:屏幕阅读器实时通知核心
观点:Toast 的无障碍基础是 ARIA live regions,它允许屏幕阅读器在内容更新时自动播报,而不窃取焦点。对于非紧急通知(如成功消息),优先使用 polite 模式,避免打断用户当前任务;紧急错误用 assertive。
证据:MDN 文档指出,aria-live="polite" 在用户暂停时播报,"assertive" 立即中断;结合 role="status" 或 "alert" 增强语义。
落地参数与清单:
- 容器设置:创建一个固定定位的
<ul id="toast-container" aria-live="polite" aria-atomic="true" role="status" aria-relevant="additions text">,初始为空。每个新 Toast 追加<li role="status">消息</li>,屏幕阅读器自动朗读整个原子区域。 - 优先级阈值:成功 / 信息类:polite(默认);错误 / 警告:assertive(动态切换容器属性);日志流:off,仅焦点时读。
- 避免 pitfalls:勿滥用 assertive(中断率 <5%);更新前设 aria-busy="true",完成后 false 防部分播报。
- 测试清单:用 NVDA/JAWS 验证播报时机、文案清晰(<20 字);确保不干扰表单焦点。
此设计确保 95% 屏幕阅读器(如 VoiceOver)兼容,引用率达 100% 无需用户导航。
CSS Animations:Enter/Exit 与 Stacking 流畅过渡
观点:动画提升感知质量,但需尊重 prefers-reduced-motion,并支持中断式 stacking。借鉴 Emil Kowalski 的 Sonner,使用 CSS transitions + data 属性实现可重定向动画,避免 keyframes 跳跃。
证据:Emil 在构建 toast 时发现 keyframes 不可中断,新 Toast 添加时旧 Toast 需平滑 reposition;transitions 支持 retargeting。
落地参数与清单:
- Enter/Exit:
.toast { position: absolute; opacity: 0; transform: translateY(100%); transition: all 0.3s cubic-bezier(0.21,1.02,0.73,1); } .toast[data-entered="true"] { opacity: 1; transform: translateY(0); } .toast[data-exiting="true"] { opacity: 0; transform: translateY(-100%); }- 时长:enter 0.3s,exit 0.4s;easing:cubic-bezier (.21,1.02,.73,1) 模拟 spring。
- Stacking:父容器的
--gap: 8px; --scale-factor: 0.05;,JS 计算每个 toast 的--offset: calc(var(--index) * (var(--toast-height) + var(--gap))); --scale: calc(1 - var(--index) * var(--scale-factor)); transform: translateY(var(--offset)) scale(var(--scale));- 深度感:index >0 时微缩放;高度统一:max-height: 80px; overflow: hidden。
- 媒体查询:
@media (prefers-reduced-motion: reduce) { transition: none; } - 参数:max-toasts=5(超限 dismiss 最早);gap=8px;lift-amount=4px(hover 展开偏移)。
JS 中:requestAnimationFrame 后设 data-entered,setTimeout (4000, () => set data-exiting & remove)。
Keyboard Focus Trap 与交互增强
观点:Toast 应支持键盘访问,但不抢焦点。除非紧急,保持 tabindex=-1;提供 ESC 关闭、hover/focus 暂停超时,实现 trap 只在多按钮时。
落地参数与清单:
- 焦点管理:普通 Toast tabindex=-1(仅 ARIA 读);互动 Toast(如含 Undo 按钮)tabindex=0,focusin 暂停定时器。
- 陷阱逻辑:若 Toast 内有多个焦点元素(如按钮),用 JS 循环:
addEventListener('keydown', e => { if (e.key==='Tab' && !shift && lastElement) e.preventDefault(), firstElement.focus(); })。 - ESC 关闭:全局 / 容器 keydown ESC dismiss 当前 / 所有。
- 参数:focus-pause-threshold=0(立即暂停);swipe-threshold=50px(移动端手势)。
Timeout Handling:智能暂停与回滚
观点:默认 4s 超时易错过,需 pause on hover/focus/tab-hidden,支持无限期(duration=Infinity)。
落地清单:
- 定时器:
let timer = setTimeout(dismiss, duration);hover/focus: clearTimeout(timer); visibilitychange: if (document.hidden) pause。 - 恢复:mouseleave/focusout/blur 重启。
- 参数:duration=4000ms(成功),6000ms(错误);max-queue=5;velocity-dismiss=0.11(快速滑动阈值)。
- 监控:MutationObserver 观察容器变化,动态 calc offset;PerformanceObserver 限 FPS<60 降级动画。
回滚策略:若 stacking 冲突,fallback 无动画 append/prepend;ARIA 失效用 fallback alert ()。
完整代码骨架
<div id="toast-root" aria-live="polite" aria-atomic="true" role="status" style="position:fixed;top:20px;right:20px;z-index:9999;"></div>
JS API: showToast('消息', {type:'success', duration:4000});
此方案体积 <2KB,性能优异,适用于生产。
资料来源:Emil Kowal.ski 的 Sonner toast 构建经验;MDN ARIA live regions 规范。