一个星形图标,鼠标点击后能变色,看起来已经完成了"收藏按钮"。但用户如果只能用键盘,能否移动到它并激活?辅助技术能否知道它做什么,以及当前是否已选中?
以米米商聊为背景,已整理的手机端资料包含消息收藏操作。本文借这一场景制作独立的浏览器按钮示例,不代表产品实际界面、键盘交互或实现,也不推断原生 App 和桌面客户端。示例只切换本地状态,没有发送真实收藏请求。
1. 先确定它是按钮,再处理点击行为
用普通 div 绘制星形,再绑定 click,只能说明某条鼠标路径有响应。原生 button 还提供按钮语义、正常的焦点参与方式和键盘激活行为,适合这种触发操作的控件。
W3C 按钮模式说明,按钮获得焦点后可以使用 Enter 或空格激活。对原生按钮,业务通常集中在 click 入口;不要额外在 keydown 中再执行同一份收藏操作,否则一次键盘操作可能经过两条业务路径。
这不表示页面中任何有 click 的元素都已经具备按钮能力。把元素选对,再验证实际交互,比先造一个普通容器、事后逐项补行为更容易核对。
| 需要回答的问题 | 本文示例的做法 | 不能只看什么 |
|---|---|---|
| 控件是什么 | 使用原生 button | 看起来像按钮 |
| 能否到达 | 参加正常键盘焦点顺序 | 鼠标能点中 |
| 控件做什么 | 提供明确的可访问名称 | 星形图标或颜色 |
| 当前状态是什么 | 用 aria-pressed 表达二态选择 | 只把星形涂黄 |
| 是否完成真实收藏 | 需要业务接口确认 | 本地演示状态已切换 |

2. 图标、名称和焦点提示分别负责什么
下面使用星形字符作装饰,并从辅助技术的名称计算中隐藏这个装饰。按钮的可访问名称由 aria-label 提供。空间允许时,可直接显示"收藏"等文字,让图标含义更容易被理解;本例特意展示只有图标的情况。
html
<style>
#favorite:focus-visible {
outline: 3px solid #2357b7;
outline-offset: 4px;
}
</style>
<form id="demo-form">
<button id="favorite" type="button"
aria-label="收藏这条消息" aria-pressed="false">
<span aria-hidden="true">☆</span>
</button>
</form>
<p id="favorite-status" role="status">未收藏(本地演示)</p>
MDN 的 button 说明讨论了图标按钮需要可访问名称的问题。不要仅凭开发者自己看得懂星形,就认为所有用户也能得到相同含义。
本例显式写 type="button",因为收藏演示不是表单提交。把操作按钮放入表单时,类型要与用途一致,避免激活控件时顺带触发表单行为。
焦点边框用于告诉键盘用户"下一次激活的是哪里"。本例通过 :focus-visible 给出明确边框,没有把焦点样式直接移除。实际项目还需结合背景、缩放、布局及不同交互方式验证可见性,不能仅凭写了一个选择器就宣称全部场景合格。
3. 二态按钮保留名称,用状态描述是否选中
本文采用固定名称"收藏这条消息",再用 aria-pressed 的 true/false 表示是否选中。星形外观和旁边的演示提示一起更新,避免只有一处状态变化。
javascript
function bindFavoriteDemo(button, status) {
let selected = false;
const icon = button.querySelector('[aria-hidden="true"]');
const render = () => {
button.setAttribute("aria-pressed", String(selected));
icon.textContent = selected ? "★" : "☆";
status.textContent = selected
? "已收藏(本地演示)"
: "未收藏(本地演示)";
};
const onClick = () => {
selected = !selected;
render();
};
render();
button.addEventListener("click", onClick);
return () => button.removeEventListener("click", onClick);
}
在元素就绪后,把按钮与提示元素传给 bindFavoriteDemo(button, status),保存返回的清理函数。一个按钮实例只绑定一次,结束使用时清理监听;本例每次新建绑定都从未收藏开始,不提供持久化或真实初始状态读取。
aria-pressed 文档说明了切换按钮的按压状态与名称关系。本文选择的是固定名称加状态的模式;如果设计选择改变操作名称,就应整体重新检查语义,而不是混用两套表达。
代码只有一个业务事件入口,没有用自制键盘监听模拟原生按钮。状态提示也是纯本地结果,文字明确写了"本地演示",不冒充服务端确认。
4. 点一次切换一次,不等于异步业务已经完成
这个最小例子用于检查交互结构,不实现网络、失败重试、重复请求控制或多端同步。真实收藏入口需要根据已有业务状态初始化,并明确何时显示请求中、何时确认成功、失败后是否恢复。
若请求期间禁止再次激活,应选择并验证对应的禁用方案。原生 disabled 会影响控件可操作性和焦点路径;只添加 aria-disabled 则不能代替阻止业务动作。不要只改一个辅助属性,就认为点击和键盘路径都已经被拦截。
同样,本例只是切换图标,不移除消息、不关闭弹窗,因此键盘焦点可以继续留在按钮上。如果真实动作会移除控件或更换上下文,就需要另外设计焦点去向。把焦点落到已经不存在的元素上,会让后续操作难以理解。
这里也没有为页面全局拦截 Enter 或空格。控件自己的激活行为与页面快捷键应分别设计,避免用户在其他位置操作时意外触发收藏。
5. 验证入口、名称和状态,而不是只截图看星形
本轮把文中的 HTML、样式和 JavaScript 原样提取到独立本地页面,使用当前内置浏览器检查。页面外围另设激活计数与表单提交计数,用来观察一次操作走了几次入口;这些计数不是产品界面或收藏服务。
| 检查路径 | 预期 |
|---|---|
| Tab 到达收藏按钮 | 当前焦点落到该控件,焦点提示可见 |
| 在按钮上按 Enter | 产生一次激活,状态由 false 变 true |
| 在按钮上按空格 | 再产生一次激活,状态由 true 变 false |
| 鼠标点击 | 同样进入 click 处理,状态相应切换 |
| 激活前后读取按钮名称 | 保持"收藏这条消息" |
| 检查图标与提示 | 与 aria-pressed 对应 |
| 按钮位于演示表单中 | 激活不增加表单提交计数 |
| 清理监听后激活 | 原来的演示逻辑不再执行 |
函数还用独立的事件目标替身检查初始呈现、连续切换、实例状态隔离和监听清理。替身检查只验证函数逻辑,不能冒充真实键盘测试;真实浏览器路径另外执行并保留记录。
浏览器可访问性树出现名称和状态,也不等于已经验证读屏软件实际播报。本轮没有进行真实读屏、手机辅助功能、跨浏览器或产品客户端验收,不能扩大为全面可访问性认证。
6. 让"是什么、做什么、现在怎样"都能被理解
图标只是视觉表达。控件语义、名称、激活方式、当前状态和焦点位置共同决定用户能否完成操作;任何一项缺失,都可能让鼠标截图看上去正常,实际使用却断在某个入口。
把操作集中到清晰的入口,再分别验证鼠标、键盘、名称与状态,可以让问题更早暴露。之后接入真实业务时,继续区分本地交互变化和服务端结果,才能完整说明一次收藏操作发生了什么。
本文为独立教学示例,部分内容由 AI 辅助整理,代码与发布内容经独立核对。