让我们来谈谈禁用按钮。具体来说,让我们深入了解一下为什么我们要使用它们,以及如何比 HTML 中传统的 disabled
属性(例如 <button disabled>
)做得更好,以标记按钮为禁用状态。
在许多用例中,禁用按钮非常有意义,我们将在稍后讨论这些原因。但在这篇文章中,我们将查看一个允许我们向购物车添加一定数量票证的表单。

这是一个很好的基线示例,因为“添加到购物车”按钮有一个明确的禁用情况:当没有票证可以添加到购物车时。
但首先,为什么禁用按钮?
阻止人们执行无效或不可用的操作是使用禁用按钮的最常见原因。在下面的演示中,我们只有在添加到购物车的票证数量大于零时才能添加票证。试试看
请允许我跳过此演示中的代码解释,并将注意力集中在重要的事情上:“添加到购物车”按钮。
<button type="submit" disabled="disabled">
Add to cart
</button>
此按钮被 disabled
属性禁用。(请注意,这是一个布尔属性,这意味着它可以写成 disabled
或 disabled="disabled"
。)
一切看起来都很好……那么有什么问题呢?
说实话,我可以在此结束这篇文章,要求您不要使用禁用按钮,因为它们很糟糕,而应该使用更好的模式。但让我们现实一点:有时禁用按钮是最合理的解决方案。
话虽如此,出于演示目的,我们将假装禁用“添加到购物车”按钮是最佳解决方案(**剧透警报:**不是)。我们仍然可以使用它来学习它的工作原理,并在此过程中改进其可用性。
交互类型
我想澄清一下,我所说的禁用按钮可用是什么意思。你可能会想,“如果按钮被禁用,它就不应该可用,那么……有什么问题呢?”请耐心听我解释。
在网络上,有多种与页面交互的方式。使用鼠标是最常见的一种,但还有其他方式,例如视力正常的人由于运动障碍而使用键盘进行导航。
尝试仅使用 Tab
键向前导航,使用 Tab
+ Shift
向后导航,来浏览上面的演示。您会注意到,禁用的按钮被跳过了。焦点直接从票证输入转移到“虚拟条款”链接。
Tab
键,焦点从输入更改为链接,跳过“添加到购物车”按钮。让我们暂停一下,回顾一下导致我们禁用按钮的原因,以及我们实际实现的效果。
通常将“交互”与“点击”联系在一起,但它们是两个不同的概念。是的,点击是一种交互方式,但它只是众多交互方式中的一种,例如悬停和聚焦。
换句话说……
所有点击都是交互,但并非所有交互都是点击。
我们的目标是阻止点击,但使用 disabled
,我们不仅阻止了点击,还阻止了聚焦,这意味着我们可能造成的伤害与益处一样多。当然,这种行为可能看起来无害,但它会导致混淆。认知障碍的人可能难以理解为什么他们无法聚焦按钮。
在下面的演示中,我们稍微调整了一下布局。如果您使用鼠标并在提交按钮上悬停,就会显示一个工具提示,解释为什么按钮被禁用。太好了!
但是,如果您只使用键盘,则无法看到该工具提示,因为按钮无法使用 disabled
聚焦。在触摸设备中也会发生同样的事情。糟糕!
Tab
键时,工具提示不见了。请允许我再次跳过代码解释。我强烈建议阅读 Haydon Pickering 的“包容性工具提示” 和 Sarah Higley 的“WCAG 2.1 时代的工具提示”,以充分了解工具提示模式。
ARIA 助攻
<button>
中的 disabled
属性功能过于强大。我知道的极少数情况下,原生 HTML 属性弊大于利,这便是其中之一。使用 ARIA 属性可以更好地完成任务,使我们能够指示屏幕阅读器如何解释按钮,但要始终如一地这样做,才能为更多人和其他用例创造一种包容性的体验。
disabled
属性对应于 aria-disabled="true"
。再次尝试下面的演示,只使用键盘。请注意,尽管按钮被标记为禁用,但它仍然可以通过焦点访问,并且会触发工具提示!
Tab
键,“添加到购物车”按钮会获得焦点,并显示工具提示。很酷吧?小小的调整,带来巨大的改进!
但我们还没有完全结束。这里需要注意的是,我们仍然需要使用 JavaScript 以编程方式阻止点击。
elForm.addEventListener('submit', function (event) {
event.preventDefault(); /* prevent native form submit */
const isDisabled = elButtonSubmit.getAttribute('aria-disabled') === 'true';
if (isDisabled || isSubmitting) {
// return early to prevent the ticket from being added
return;
}
isSubmitting = true;
// ... code to add to cart...
isSubmitting = false;
})
您可能熟悉这种模式,它可以防止双击两次提交表单。如果您出于此原因使用 disabled
属性,我更倾向于不这样做,因为这会导致表单提交时暂时失去键盘焦点。
disabled
和 aria-disabled
之间的区别
您可能会问:如果 aria-disabled
默认情况下不阻止点击,那么使用它的意义何在?要回答这个问题,我们需要了解这两个属性之间的区别
功能 / 属性 | disabled | aria-disabled="true" |
---|---|---|
阻止点击 | ✅ | ❌ |
阻止悬停 | ❌ | ❌ |
阻止焦点 | ✅ | ❌ |
默认 CSS 样式 | ✅ | ❌ |
语义 | ✅ | ✅ |
两者之间唯一的重叠是语义。这两个属性都会宣布按钮确实已禁用,这很好。
与 disabled
属性相反,aria-disabled
完全是关于语义的。ARIA 属性默认情况下永远不会更改应用程序行为或样式。它们唯一的目的是帮助辅助技术(例如屏幕阅读器)以更有意义和更健壮的方式宣布页面内容。
到目前为止,我们已经讨论了两种类型的人,点击的人和使用 Tab
键的人。现在,让我们讨论另一种类型:那些有视力障碍(例如失明、弱视)并使用屏幕阅读器浏览网络的人。
使用屏幕阅读器的人通常更喜欢使用 Tab 键导航表单字段。现在,看看 macOS 上的 VoiceOver 如何完全跳过 disabled
按钮。
Tab
键时会跳过“添加到购物车”按钮。再次强调,这是一个非常小的表单。在一个更长的表单中,立即找不到提交按钮会让人很烦躁。想象一个表单,其中提交按钮被隐藏,只有在您完全填写表单时才会显示。这就是有些人使用 disabled
属性时所感受的。
幸运的是,使用 disabled
的按钮不会完全无法被屏幕阅读器访问。您仍然可以逐个单独导航每个元素,最终会找到按钮。
虽然这是一种可行的做法,但这是一种让人恼火的体验。另一方面,使用 aria-disabled
,屏幕阅读器会像往常一样聚焦按钮,并正确地宣布其状态。请注意,不同屏幕阅读器之间的宣布方式略有不同。例如,NVDA 和 JWAS 会说“按钮,不可用”,但 VoiceOver 会说“按钮,变暗”。
Tab
键的“添加到购物车”按钮,因为使用了 aria-disabled
。我已经绘制了这两个属性基于我们刚刚使用的工具创建的不同用户体验的映射
工具 / 属性 | disabled | aria-disabled="true" |
---|---|---|
鼠标或轻触 | 阻止按钮点击。 | 需要 JS 来阻止点击。 |
Tab | 无法聚焦按钮。 | 能够聚焦按钮。 |
屏幕阅读器 | 按钮难以定位。 | 按钮很容易找到。 |
因此,这两个属性之间主要区别是
disabled
可能会在使用Tab
键时跳过按钮,从而导致混乱。aria-disabled
仍然会聚焦按钮并宣布它的存在,但它尚未启用;就像您在视觉上感知它一样。
这就是承认可访问性和可用性之间细微差别的重要之处。可访问性衡量的是某人是否能够使用某物。可用性衡量的是某物使用起来有多容易。
鉴于此,disabled
是否可访问?是的。它的可用性好吗?我不这么认为。
我们可以做得更好吗?
如果我在不向您展示我们门票表单示例的真正包容性解决方案的情况下结束这篇文章,我会感到不安。尽可能不要使用禁用按钮。让用户随时点击它,如果需要,显示错误消息作为反馈。这种方法还可以解决其他问题
- 更少的认知摩擦:允许用户随时提交表单。这消除了按钮是否被禁用的不确定性。
- 颜色对比度:虽然禁用按钮不需要满足 WCAG 1.4.3 颜色对比度,但我认为我们应该保证元素始终可见,无论其状态如何。但现在我们不必担心这个问题,因为按钮不再被禁用。
最后的想法
<button>
中的 disabled
属性是一个奇怪的情况,尽管它在社区中广为人知,但它可能不是解决特定问题的最佳方法。不要误解我,因为我不是说 disabled
总是不好的。仍然有一些情况下使用它是有意义的(例如分页)。
说实话,我不认为 disabled
属性完全是一个可访问性问题。让我更担心的是可用性问题。通过用 aria-disabled
替换 disabled
属性,我们可以使用户的体验更加愉快。
这只是我迈向网络可访问性之旅的又一步。多年来,我发现可访问性远不止遵守网络标准。处理用户体验很棘手,大多数情况下需要权衡和妥协我们在如何处理解决方案方面的方法。没有完美的可访问性的灵丹妙药。
我们作为网络创建者的职责是寻找和理解可用的不同解决方案。只有这样,我们才能做出最好的选择。假装问题不存在毫无意义。
最后,请记住,没有什么能阻止您使网络更具包容性。
奖励
还在吗?让我提一下关于这个演示的最后两件事,我认为它们很有价值
1. 实时区域将宣布动态内容
在演示中,内容的两个部分发生了动态变化:表单提交按钮和成功确认(“已添加 [X] 张票!”)。
这些变化在视觉上是可以感知的,但是,对于使用屏幕阅读器的视障人士来说,这并不是现实。为了解决这个问题,我们需要将这些消息变成实时区域。这些允许辅助技术监听更改,并在更改发生时宣布更新的消息。
在演示中有一个 .sr-only
类,它隐藏了一个包含加载消息的 <span>
,但允许屏幕阅读器宣布它。在这种情况下,aria-live="assertive"
应用于 <span>
,并且它在表单提交后并处于加载过程中的时候保存一个有意义的消息。这样,我们可以向用户宣布表单正在工作,并请耐心等待加载。此外,我们对表单反馈元素也做同样的事情。
<button type="submit" aria-disabled="true">
Add to cart
<span aria-live="assertive" class="sr-only js-loadingMsg">
<!-- Use JavaScript to inject the the loading message -->
</span>
</button>
<p aria-live="assertive" class="formStatus">
<!-- Use JavaScript to inject the success message -->
</p>
请注意,aria-live
属性必须从一开始就存在于 DOM 中,即使元素还没有保存任何消息,否则辅助技术可能无法正常工作。
关于这个小小的 aria-live
属性以及它所做的大事情,还有更多内容要告诉你。也有一些陷阱。例如,如果它被错误地应用,它会弊大于利。值得阅读 Ire Aderinokun 的 “使用 aria-live” 以及 Adrian Roselli 的 “加载骨架”,以更好地了解它是如何工作的以及如何使用它。
pointer-events
来阻止点击
2. 不要使用 这是我在网上看到的一种替代(且不正确)的实现方式。它在 CSS 中使用 pointer-events: none;
来阻止点击(没有任何 HTML 属性)。请不要这样做。这是一个 丑陋的 Pen,希望能够证明为什么。我再说一遍,不要这样做。
尽管该 CSS 确实阻止了鼠标点击,但请记住,它不会阻止聚焦和键盘导航,这会导致意外结果,甚至更糟糕的是错误。
换句话说,使用此 CSS 规则作为阻止点击的策略是毫无意义的(明白了吗?)。;)
使用 HTML5 表单验证功能怎么样?这将为非禁用用户和禁用用户带来更好的体验。
它也可能是这样。尽管它有样式限制,而且我担心它在不同的浏览器中的行为会有所不同。
在手机上阅读这篇文章显示了您使用的每种方法的一个主要局限性 - 它们都不是明显地被禁用。在手机上没有悬停或聚焦,因此如果没有第二个启用按钮与之形成对比,就无法判断按钮是否被禁用。
是的,这是
disabled
的另一个限制!(我会在文章中添加这个缺点)。但是,使用aria-disabled
,您仍然应该在点击“添加到购物车”时看到工具提示。(在 iPhone,IOS 13 上测试)这是一个很好的总结,我很喜欢它,事实上我创建了一个 Glimmer/Ember 修饰符,让用户更容易使用这个想法(GitHub,npm),并且还演示了相同想法的例子 在 Svelte REPL 中。希望这些对阅读这篇文章的其他用户有所帮助。非常感谢您发表这篇精彩的文章!
这是否也包括复选框之类的表单元素?
此外,如果正确完成,验证可以在这方面使用。默认情况下,它不会像这篇文章中建议的那样工作。但是,如果表单是 novalidate,并且 JS 将自定义验证消息填充到表单中的实时区域,它将像这篇文章中建议的那样工作。
这里只关注按钮。对于其他元素,则取决于场景:)
确实可以使用本机验证,但前提是我们不使用禁用按钮。使用禁用按钮,我担心验证甚至不会被触发。
我看到过对“尽可能不要使用禁用按钮。让用户随时点击它,如果需要,显示错误消息作为反馈”的反对意见。您是否有任何资源/研究可以帮助完善这种观点?
我想到的第一个(也在博客文章中提到)是 Axess Lab 的禁用按钮很糟糕。
听起来都很合理,但在 js 中阻止点击会使代码变得混乱,我们应该在大型项目中做一百次。
我也有同样的想法。如今,我看到人们用 js 来做所有事情,而忽略了 HTML 的功能。
很棒的文章,感谢你详细地写了它并解释了可访问性问题。
很棒的文章,这让我思考。如果一个按钮无法执行它的动作,它应该是什么样子?在上面的例子中,禁用的按钮仍然看起来像一个带有“添加到购物车”字样的按钮,即使按钮是灰色的,它仍然暗示按钮能够将物品添加到购物车中,而我们知道它不能。我想知道是否可以通过更改按钮文本、添加标签或完全移除按钮来提高整体可访问性,直到它能够工作。我很想听听大家对此的看法。
正如我所说,我认为这不仅仅是可访问性问题,主要是可用性问题。我真的很喜欢你提出的问题——它们触及了 UX 中的重要点。
每个案例都是独一无二的,所以无论你选择哪种方法,都要记住在做出创建新模式的决定之前,与不同的人测试它并收集他们的反馈意见:)
“或者完全移除按钮,直到它能够工作。”这就是我现在正在尝试做的,但我不知道这是否符合可访问性要求?
在上面的例子中,使用禁用的按钮文本作为“添加 1 到 9 张票”,然后当字段中有数量时,将按钮文本改为“添加到购物车”怎么样?这样按钮就可以兼顾双重功能。
在这种情况下,我认为我们正在混合元素的目的。按钮将作为“提示文本”而不是真正的按钮。从 UI/UX 的角度来看可能会令人困惑。此外,一些屏幕阅读器无法正确处理动态标签到内容的更改(参考:https://adrianroselli.com/2020/12/be-careful-with-dynamic-accessible-names.html)。
如果 javascript 不可用,是否有禁用点击操作的备用方案?或者你认为这没什么必要?显然,服务器端验证应该捕获任何丢失的数据,但这对用户来说仍然会很混乱。我想这也是避免禁用按钮的另一个原因。
尝试使用
required
和aria-required
属性。是的,在输入中添加“required”和“pattern”属性,并使用适当的值。然后浏览器将在提交表单之前进行原生验证。
非常棒的文章。它让我能够更好地在 Material-UI 中推广它(https://github.com/mui-org/material-ui/pull/27719)。
我想知道我们是否可以想出一个比“包容性禁用”更好的名称?
我对禁用的行为很好奇,所以用我手头的设备进行了测试,一台运行 iOS 15.0.2 的 iPhone。我发现,例如 2,带有工具提示的普通禁用按钮
点击按钮会显示工具提示
VoiceOver 允许导航到按钮
它将其读作“添加到购物车,变暗”
VoiceOver 允许激活按钮。它会发出一声开心的点击声。工具提示会显示,但不会在显示时被朗读。
在工具提示打开的情况下重新聚焦按钮,会将按钮的描述读作按钮的标题,在变暗的文字之后。
第一个例子,没有工具提示,也显示了变暗的按钮读数。
似乎有些技术已经变得更好,无论是移动触摸还是辅助技术,一个简单的禁用按钮现在在可访问性方面与后一个例子相当。(我很好奇这能扩展到什么程度,但我手头没有一堆设备可以用来测试。)
为什么不修复“disabled”属性,让它默认具有这种行为?那不应该是最好的解决方案吗?
好文章!
这里唯一让我困惑的是:“使用 ARIA 属性可以做得更好,让我们不仅可以将焦点放到按钮上...”。
这是一个误导性的说法。ARIA 不会添加焦点,ARIA 只会影响屏幕阅读器解释页面的方式和内容,但 ARIA 不会影响键盘导航。焦点之所以出现,仅仅是因为‘disabled’属性已从按钮中移除。请更正这一部分,以避免对 ARIA 的用途产生进一步的误解。谢谢!
Daria 说得对,这确实会让人误解。文本已编辑:)
超级有用的文章,谢谢!
对于“我们可以做得更好吗?”部分,可以进一步改进,以防止无效的输入,从而永远不会显示警告消息。可以将输入默认设置为“1”,然后将最小值限制为“1”,以确保它不能被更改回来。我认为让用户犯错通常是可以的,但在这种情况下,“0”无效是显而易见的:)