不能直接用 draggable="true" 做可访问拖拽,因其绕过焦点流、无 aria 映射、datatransfer 对辅助技术不透明;合规方案是键盘驱动的“选中-移动-插入”两步操作,并严格遵循语义结构与 aria 补丁要求。

为什么不能直接用 draggable="true" 做可访问拖拽
因为浏览器对 draggable="true" 的实现完全绕过焦点流:Tab 键进不去,Space/Enter 触发不了,屏幕阅读器读不出“可拖拽”状态,更不会播报位置变化。Chrome 和 Firefox 都没为它补 ARIA 映射,role="application" 也兜不住。dataTransfer 对象对辅助技术是黑盒,dragover 视觉高亮也不会同步到 AT。
用键盘驱动的“选中-移动-插入”两步替代原生 drag
这是目前唯一被 WCAG 2.2 Success Criterion 2.5.7 明确认可的合规路径。核心不是模拟鼠标动作,而是提供等效的键盘操作语义:
- 按
Tab进入列表,聚焦首个项(需设tabindex="0") - 按
Enter或Space进入“选中态”,此时设aria-selected="true"并加视觉高亮 - 用
↑/↓移动焦点,实时更新aria-describedby指向的提示文本,如“插入到第 2 项之前” - 按
Shift+Enter确认插入,DOM 移动后立即el.focus()到新位置,并写入aria-live="polite"区域
全程禁用 draggable="true",避免干扰键盘导航逻辑。
必须补的三处最低限度 ARIA 补丁(如果硬要用原生 drag)
审计工具会直接标为严重问题,除非覆盖以下三点:
- 每个可拖元素加
aria-roledescription="可拖拽字段"(不是已废弃的aria-grabbed),并在dragstart中动态设aria-describedby指向一个隐藏说明段落 - 目标区域监听
dragenter时,立刻更新aria-live="assertive"内容,例如:“已进入排序区域,松开鼠标可放置” -
drop后用setTimeout(() => el.focus(), 0)强制焦点移到新插入项,且该项必须有tabindex="0"
注意:aria-dropeffect="move" 已从 WAI-ARIA 1.2 中移除,任何使用它的代码都属于过时写法。
语义结构不匹配会导致整个拖拽逻辑失效
用纯 <div> 实现排序容器,会让辅助技术无法识别其目的。必须用语义化结构打底:
<ul>
<li>排序列表用 <code><ol role="list"></ol> 或 <ul role="list"></ul>,配 aria-label 描述用途
<li role="listitem">,而非 <div role="option"> —— 后者仅适用于选择上下文
<li>拖拽手柄图标(如三横线)必须带 <code>aria-label="拖拽以重排序",禁用项要设 aria-disabled="true" + tabindex="-1"
视觉反馈不能只靠 mousemove,键盘操作时得用 FLIP 技术驱动 transform,否则动画会打断焦点流,且屏幕阅读器听不到位置变更。











