无障碍
无障碍不是附加功能,而是这套外壳的默认行为;改动交互时不要破坏下面任何一条。
键盘可达
- 所有可交互元素都是原生
button/a/input,或 Radix 原语提供的可聚焦节点。 - 侧栏菜单、命令面板、下拉、树、步骤条都能只用键盘走完:
Tab移动、Enter/Space触发、Esc关闭。 - 命令面板:
⌘K打开(⌘⇧S同样可用),↑/↓选择,Enter打开结果,Esc关闭。 - 对话框拖动:聚焦标题栏后用方向键微调位置,
Shift加速,双击标题复位。 - 树节点可键盘展开 / 收起、移动到节点上、按拖拽协议排序。
焦点管理
- 对话框打开时焦点进入内容区,关闭后回到触发元素;
src/lib/dialog-focus.ts里的avoidInitialCloseFocus防止"关闭按钮抢初始焦点"导致误触。 - 浮层统一由 Radix 负责焦点陷阱与
Esc关闭,业务侧不要自己preventDefault掉这些行为。 - 对话框拖动时标题保持不可选中(否则拖拽会顺手选中文本),但也不阻止复制标题内容——这是刻意的折中。
语义与属性
- 页面结构:
<header>(页头)、<main>(内容)、<ol>(步骤条)、<table>(表格,含caption)。 - 表格:
aria-sort表达排序方向,行内展开按钮带aria-expanded,虚拟滚动只影响渲染不影响可访问树。 - 表单:
Label与控件用htmlFor/id关联,错误态走aria-invalid+text-destructive文案。 - 图标一律
aria-hidden="true",可点图标按钮必须给aria-label。 - 纯装饰图形(迷你趋势线等)提供
role="img"+aria-label,把"图形在说什么"讲清楚。
对比度
- 语义色(success / warning / info / danger)与各自
-soft面层的组合按 WCAG AA 校验。 - Playwright 里有对比度断言,改 Token 后必须重跑
pnpm test:e2e。 - 「设置 → 无障碍 → 高对比」会进一步加深边框与文字,深色模式也要成立。
用户可调的辅助选项
| 选项 | 取值 | 实现 |
|---|---|---|
| 字号 | 100 / 112.5 / 125 / 150 | data-font-scale |
| 字重 | 默认 / 中等 / 粗体 | --a11y-weight-* |
| 高对比 | 开 / 关 | data-high-contrast |
| 减少动效 | 开 / 关 | data-reduce-motion,同时尊重系统 prefers-reduced-motion |
| 链接下划线 | 开 / 关 | data-underline-links |
| 灰色模式 | 开 / 关 | data-grayscale |
| 色弱模式 | 开 / 关 | data-color-weak |
这些属性在 index.html 的内联脚本里于首屏前写入根元素,避免"先正常再变灰"的闪烁。
改动时的自检
- 只用键盘能否完成这次交互?焦点有没有掉到
body? - 深色 + 高对比下,文字与背景还能分清吗?
- 打开「减少动效」后,是否还有必须等完的动画?
- 新加的图标按钮有没有
aria-label?新加的表单控件有没有Label关联? pnpm test:e2e的对比度与键盘用例是否仍然通过?