原生draggable="true"不满足无障碍要求,因其绕过焦点流、无键盘触发、屏幕阅读器不可见;合规方案是键盘驱动的“选中→移动→插入”三步操作,需语义结构、aria补丁及严格事件响应。

原生 draggable="true" 无法满足无障碍要求,它绕过焦点流、不支持键盘触发、对屏幕阅读器完全不可见。合规做法是放弃模拟鼠标拖拽,改用键盘驱动的“选中→移动→插入”三步语义操作。
为什么不能监听 Space/Enter 后调用 element.draggable = true
这么做看似能启动拖拽,但实际会破坏键盘导航链:元素获得 tabindex 后可聚焦,但 dragstart 是鼠标事件,无法被辅助技术感知;dataTransfer 对象在 AT(辅助技术)中不可读,屏幕阅读器既不知道“正在拖”,也不知道“拖到哪了”。Chrome 和 Firefox 均未为该属性补全 ARIA 映射,role="application" 也兜不住语义缺失。
键盘替代方案必须满足的三个结构前提
没有语义结构,所有键盘逻辑都会失效:
- 排序容器必须用
<ol role="list"></ol>或<ul role="list"></ul>,配aria-label(如aria-label="任务排序列表"),禁用纯<div> <li>每项必须是 <code><li role="listitem">,不是<div role="option">——后者仅适用于选择上下文,AT 会误读为下拉选项 <li>拖拽手柄(如三横线图标)需带 <code>aria-label="拖拽以重排序",禁用状态要设aria-disabled="true"+tabindex="-1" - Tab 进入列表,首个项需有
tabindex="0";按 Enter/Space 进入“选中态”,此时设aria-selected="true"并视觉高亮 - ↑/↓ 键移动焦点时,实时更新
aria-describedby指向的提示文本,例如:“插入到第 3 项之前” - Shift+Enter 确认插入,DOM 移动后立即调
el.focus()到新位置,并写入aria-live="polite"区域播报结果 - 全程禁用
draggable="true",避免干扰键盘导航逻辑;任何dragstart/drop监听器都应移除 - 每个可拖元素加
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 中移除,任何使用它的代码都属于过时写法
keydown 事件里该监听哪些键、怎么响应
键盘操作不是“模拟拖拽”,而是表达明确意图。必须按 WCAG 2.2 SC 2.5.7 要求实现:
如果硬要用原生 drag,最低限度的 ARIA 补丁有哪些
审计工具会直接标为严重问题,除非覆盖以下三点(且仍不推荐):
真正难的不是写对这几个事件,而是让视觉反馈和 AT 反馈严格同步——比如用 FLIP 技术驱动 transform 动画,否则动画会打断焦点流,屏幕阅读器也听不到位置变更。











