以下是计划!我们将构建一个带样式的选择元素。不仅是 外部,还有内部。完全的样式控制。此外,我们将使其无障碍。我们不会尝试通过原生 <select>
元素复制浏览器默认提供的所有内容。当使用任何辅助技术时,我们将直接使用 <select>
元素。但当使用鼠标时,我们将显示带样式的版本并使其像选择元素一样工作。
这就是我所说的“混合”选择:它们在一个设计模式中同时是原生 <select>
和带样式的备用选择。

选择、下拉列表、导航、菜单……名称很重要
在为本文进行研究时,我考虑了许多在谈论选择时经常使用的名称,其中最常见的是“下拉列表”和“菜单”。我们可能会犯两种类型的命名错误:对不同的事物使用相同的名称,或对不同的事物使用相同的名称。选择可能会同时遭受这两种错误的影响。
在我们继续之前,请允许我尝试澄清使用“下拉列表”作为术语的情况。以下是我对下拉列表含义的定义
下拉列表:一个交互式组件,它包含一个按钮,该按钮显示和隐藏项目列表,通常在鼠标悬停、点击或轻触时显示。默认情况下,列表不可见,直到交互开始。该列表通常在其他内容之上显示内容块(即选项)。
许多界面看起来像下拉列表。但是仅仅将一个元素称为“下拉列表”就像用“鱼”来描述动物一样。它是什么类型的鱼?小丑鱼与鲨鱼不同。下拉列表也是如此。

就像海洋中有不同类型的鱼一样,当我们随意使用“下拉列表”一词时,我们可能正在讨论不同类型的组件
- 菜单:用户可以在页面内容中执行的命令或操作列表。
- 导航:用于在网站中导航的链接列表。
- 选择:一个表单控件(
<select>
),它显示一个选项列表,供用户在表单中选择。
确定我们正在讨论哪种类型的下拉列表可能是一项模糊的任务。以下是一些来自网络的示例,它们与我对这三种不同类型的分类方式相匹配。这是基于我的研究,有时,当找不到合适的答案时,基于我的经验的直觉。

图表标签 | 场景 | 下拉列表类型 |
---|---|---|
1 | 下拉列表期望在表单上下文中提交选定的选项(例如,选择年龄) | 选择 |
2 | 下拉列表不需要活动选项(例如,操作列表:复制、粘贴和剪切) | 菜单 |
3 | 选定的选项会影响内容。(例如,排序列表) | 菜单或选择(稍后详细介绍) |
4 | 下拉列表包含指向其他页面的链接。(例如,包含网站链接的“超级导航”) | 公开导航 |
5 | 下拉列表包含非列表内容。(例如,日期选择器) | 其他不应该称为下拉列表的内容 不应该称为下拉列表 |
并非每个人都以相同的方式感知和交互互联网。命名用户界面和定义设计模式是一个基本过程,但它也存在很大的个人解读空间。所有这些差异都是“下拉列表世界”的驱动力。
有一种下拉列表类型显然是菜单。它的用法是关于无障碍性的对话中的热门话题。我在这里不会过多讨论它,但让我重申一下 <menu>
元素已弃用,不再推荐。以下是对 包容性菜单和菜单按钮的详细说明,包括为什么 不应将 ARIA 菜单角色用于站点导航。
我们甚至还没有接触到其他属于相当灰色区域的元素,由于 WCAG 社区缺乏实际用例,这些元素使下拉列表的分类变得更加模糊。
哇……内容很多。让我们忘记这个“下拉列表世界”的混乱,并专注于显然是 <select>
元素的下拉列表类型。
<select>
让我们谈谈 为表单控件设置样式是一段有趣的旅程。正如 MDN 所述,存在 好、坏和丑。好的是像 <form>
这样的东西,它只是一个块级元素,可以设置样式。坏的是像复选框这样的东西,可以完成,但有点麻烦。<select>
绝对处于丑陋的领域。
已经写了很多关于它的文章,即使在 2020 年,创建自定义选择仍然是一个挑战,并且 一些用户仍然更喜欢简单的原生选择。
在开发人员中,<select>
是 迄今为止最令人沮丧的表单控件,主要是因为 它缺乏样式支持。其背后的 UX 挑战非常大,以至于我们 寻找其他替代方案。好吧,我想 <select>
的第一条规则 类似于 ARIA:如果可以,请避免使用它。
我可以在此结束本文,内容为“不要使用 <select>
,就这么简单。”但是让我们面对现实:在许多情况下,选择仍然是我们最好的解决方案。这可能包括我们需要处理包含大量选项的列表、空间有限的布局或根本没有时间或预算从头开始设计和实现出色的自定义交互式组件的场景。
<select>
需求
自定义 当我们决定创建自定义选择时——即使它只是一个“简单的”选择——我们通常需要处理以下需求
- 有一个按钮包含当前选定的选项。
- 点击该框会切换选项列表(也称为列表框)的可见性。
- 点击列表框中的选项会更新选定的值。按钮文本会更改,并且列表框会关闭。
- 点击组件外部会关闭列表框。
- 触发器包含一个指向下方的三角形小图标,以指示存在选项。
类似于以下内容
你们中的一些人可能认为这可以工作并且一切正常。但是等等……它对所有人都有效吗?并非所有人都使用鼠标(或触摸屏)。此外,原生 <select>
元素附带了更多我们免费获得的功能,而这些功能并未包含在这些需求中,例如
- 无论用户的视觉能力如何,选定的选项都对所有用户可见。
- 该组件可以通过键盘以可预测的方式与所有浏览器交互(例如,使用箭头键导航,
Enter
键选择,Esc
键取消等)。 - 辅助技术(例如屏幕阅读器)会向用户清晰地宣布该元素,包括其角色、名称和状态。
- 列表框位置已调整。(即不会被屏幕截断)。
- 该元素尊重用户的操作系统首选项(例如高对比度、配色方案、运动等)。
大多数自定义选择都会以某种方式失败。请查看一些主要的 UI 组件库。我不会提及任何库,因为网络是瞬息万变的,但请尝试一下。您可能会注意到,一个框架中的选择组件的行为与另一个框架中的不同。
以下是一些需要关注的其他特性
- 使用键盘导航时,列表框选项是否会在获得焦点时立即激活?
- 您可以使用
Enter
和/或Space
键选择选项吗? Tab
键是否会跳转到列表框中的下一个选项,还是跳转到下一个表单控件?- 使用箭头键到达列表框中的最后一个选项时会发生什么?它是否只是停留在最后一个项目上,是否会返回到第一个选项,或者最糟糕的是,焦点是否会移动到下一个表单控件?
- 是否可以使用
Page Down
键直接跳转到列表框中的最后一个项目? - 如果项目多于当前视图中的项目,是否可以滚动浏览列表框项目?
这是原生 <select>
元素中包含的功能的一个小样本。
一旦我们决定创建自己的自定义选择,我们就强迫人们以某种方式使用它,而这种方式可能并非他们期望的方式。
但情况更糟。甚至原生的<select>
在不同的浏览器和屏幕阅读器中表现也不同。一旦我们决定创建自己的自定义选择器,我们就强迫人们以某种方式使用它,而这可能不是他们期望的方式。这是一个危险的决定,魔鬼就隐藏在这些细节中。
构建“混合”选择器
当我们构建一个简单的自定义选择器时,我们是在不知不觉中做出了权衡。具体来说,**我们牺牲了功能性来换取美观性。** 应该是相反的。
如果我们默认提供一个原生选择器,并在可能的情况下用一个更美观的替换它呢?这就是“混合”选择器的理念发挥作用的地方。它被称为“混合”,因为它由两个选择器组成,在适当的时候显示合适的那个。
- 一个原生选择器,默认情况下可见且可访问。
- 一个自定义选择器,隐藏起来,直到可以用鼠标安全地与其交互。
让我们从标记开始。首先,我们将添加一个原生<select>
,其中包含<option>
项,在自定义选择器之前,才能使其工作。(我稍后会解释原因。)
任何表单控件都必须有一个描述性标签。我们可以使用<label>
,但这会在点击标签时聚焦原生选择器。为了防止这种行为,我们将使用<span>
并使用aria-labelledby
将其连接到选择器。
最后,我们需要告诉辅助技术忽略自定义选择器,使用aria-hidden="true"
。这样,无论如何,只有原生选择器会被它们宣布。
<span class="selectLabel" id="jobLabel">Main job role</span>
<div class="selectWrapper">
<select class="selectNative js-selectNative" aria-labelledby="jobLabel">
<!-- options -->
<option></option>
</select>
<div class="selectCustom js-selectCustom" aria-hidden="true">
<!-- The beautiful custom select -->
</div>
</div>
这将我们带到了样式方面,在这里我们不仅使事物看起来漂亮,而且处理从一个选择器到另一个选择器的切换。我们只需要几个新的声明就可以让所有的魔法发生。
首先,原生和自定义选择器必须具有相同的宽度和高度。这确保了当切换发生时,人们不会看到布局上的重大差异。
.selectNative,
.selectCustom {
position: relative;
width: 22rem;
height: 4rem;
}
有两个选择器,但只有一个可以决定容纳它们的区域。另一个需要绝对定位,将其移出文档流。让我们对自定义选择器这样做,因为它是在可以的情况下才使用的“替换”。我们默认情况下会将其隐藏起来,这样任何人都无法立即访问它。
.selectCustom {
position: absolute;
top: 0;
left: 0;
display: none;
}
这里来了“有趣”的部分。我们需要检测用户是否正在使用悬停是主要输入方式的设备,例如带有鼠标的计算机。虽然我们通常认为媒体查询用于响应式断点或检查功能支持,但我们也可以使用它来检测悬停支持,使用@media query (hover :hover)
,这受所有主要浏览器支持。因此,让我们仅在具有悬停功能的设备上显示自定义选择器。
@media (hover: hover) {
.selectCustom {
display: block;
}
}
很好,但是对于即使在具有悬停功能的设备上也使用键盘进行导航的用户来说呢?我们将做的是,当原生选择器处于焦点时隐藏自定义选择器。我们可以使用相邻兄弟组合器(+
)。当原生选择器处于焦点时,隐藏DOM顺序中紧随其后的自定义选择器。(这就是为什么原生选择器应该放在自定义选择器之前的原因。)
@media (hover: hover) {
.selectNative:focus + .selectCustom {
display: none;
}
}
就是这样!在两个选择器之间切换的技巧完成了!当然,还有其他CSS方法可以做到这一点,但这很好用。
最后,我们需要一些JavaScript。让我们添加一些事件监听器。
- 一个用于点击事件,触发自定义选择器打开并显示选项。
- 一个用于同步两个选择器的值。当一个选择器的值发生变化时,另一个选择器的值也会更新。
- 一个用于基本的键盘导航控件,例如使用
向上
和向下
键导航,使用Enter
或空格
键选择选项,以及使用Esc
关闭选择器。
可用性测试
我进行了一个非常小的可用性测试,我让一些残疾人尝试了混合选择器组件。使用Chrome(81)、Firefox(76)和Safari(13)的最新版本测试了以下设备和工具。
- 仅使用鼠标的桌面设备。
- 仅使用键盘的桌面设备。
- MacOS 上使用键盘的VoiceOver。
- Windows 上使用键盘的NVDA。
- iPhone 和iPad 上使用Safari的VoiceOver。
所有这些测试都按预期工作,但我认为**可以使用更多样化的人员和工具进行更多可用性测试**。如果您有权访问其他设备或工具(例如JAWS、Dragon等),请告诉我测试结果。
在测试过程中发现了一个问题。具体来说,问题出在VoiceOver设置“鼠标指针:移动VoiceOver光标”上。如果用户用鼠标打开选择器,则会打开自定义选择器(而不是原生选择器),用户将无法体验原生选择器。
我最喜欢这种方法的地方在于,它在不影响核心功能的情况下利用了两个世界的优势。
- 移动设备和平板电脑上的用户获得原生选择器,这通常比自定义选择器提供更好的用户体验,包括性能优势。
- 键盘用户可以按照预期的方式与原生选择器进行交互。
- 辅助技术可以像往常一样与原生选择器进行交互。
- 鼠标用户可以与增强的自定义选择器进行交互。
这种方法**为每个人提供了必要的原生功能**,而无需额外付出巨大的代码工作来实现所有原生功能。
不要误解我的意思。此技术不是一劳永逸的解决方案。它可能适用于简单的选择器,但可能不适用于涉及复杂交互的情况。在这些情况下,我们需要使用ARIA和JavaScript来弥补差距,并创建一个真正可访问的自定义选择器。
关于看起来像菜单的选择器的说明
让我们回顾一下第三种下拉菜单场景。如果您还记得,它是一个始终具有选中选项的下拉菜单(例如,对某些内容进行排序)。我将其归类为灰色区域,既可以是菜单也可以是选择器。
我的思路是:几年前,这种下拉菜单主要使用原生<select>
实现。如今,通常会看到它使用自定义样式从头开始实现(可访问与否)。我们最终得到的是一个看起来像菜单的选择器元素。

**<select>
是一种菜单。**两者具有相似的语义和行为,尤其是在涉及选项列表且始终选中一个选项的场景中。现在,让我提一下WCAG 3.2.2 输入时(A级)准则。
更改任何用户界面组件的设置不应自动导致上下文更改,除非用户在使用组件之前已收到有关该行为的通知。
让我们将其付诸实践。想象一个可排序的学生列表。从视觉上看,排序是即时的,这可能很明显,但这并不一定对每个人都适用。因此,当使用<select>
时,我们有违反WCAG指南的风险,因为页面内容发生了变化,并且显着重新排列页面内容被认为是上下文更改。
为了确保准则成功,我们必须在用户与元素交互之前警告用户操作,或在选择器后立即包含一个<button>
以确认更改。
<label for="sortStudents">
Sort students
<!-- Warn the user about the change when a confirmation button is not present. -->
<span class="visually-hidden">(Immediate effect upon selection)</span>
</label>
<select id="sortStudents"> ... </select>
也就是说,在更改页面内容的简单菜单方面,使用<select>
或构建自定义菜单都是很好的方法。请记住,您的决定将决定使组件完全可访问所需的工作量。这是一种可以使用混合选择器方法的场景。
结束语
整个想法最初只是一个无辜的CSS技巧,但经过所有这些研究,我再次被提醒,在不损害可访问性的情况下创建独特的体验并非易事。
构建真正可访问的选择器组件(或任何类型的下拉菜单)比看起来要难。WCAG提供了优秀的指导和最佳实践,但如果没有具体的示例和多样化的实际用例,这些指南大多是理想化的。更不用说ARIA支持不温不火,并且原生<select>
元素在不同浏览器中的外观和行为也不同。
“混合”选择器只是另一种尝试,旨在创建一个美观的选择器,同时获得尽可能多的原生功能。不要将此技术实验视为淡化可访问性的借口,而应将其视为服务于两个世界的尝试。如果您有资源、时间和必要的技能,请正确地去做,并在将组件发布到世界各地之前确保与不同的用户进行测试。
附注:制作“下拉菜单”组件时,请记住使用正确的名称。😉
在设计师批准后,我喜欢使用
<datalist>
,它可以做很多难以模拟的事情。但是,就像<select>
一样,样式选项会很好。我真的很喜欢使用混合而不是使用原生元素的想法。
我多次遇到过这样的问题:我希望自定义元素既可访问又可交互,但随后我不得不坚持使用原生元素仅仅是为了可访问性。
但现在读完这篇文章后,我想我会使用混合方法。
两全其美,几乎没有牺牲。
有趣的模式,之前不得不处理自定义选择框,我不知道我怎么会没想到这个解决方案,谢谢!
我想知道是否可以使用类似的方法来创建具有原生功能的可访问对话框元素。
这篇文章可能对您有所帮助! https://css-tricks.cn/a-css-approach-to-trap-focus-inside-of-an-element/
排序实际上并不是一个足以构成上下文变化的大变化。
内容变化 != 上下文变化。
请参阅输入下方的第二个示例。
上下文变化就像从关于页面跳转到联系页面、打开一个新窗口或将当前焦点切换到另一个元素。
(请参阅“上下文变化”的官方定义)
如果您使用
<select>
作为主要导航,则该导航不可访问。但是,如果您只是用它来立即更改新闻文章列表的排序,则是可访问的。
不过,无论如何,提醒用户其立即影响可能也没有坏处。
感谢 Daniel 的反馈!我可能要承认,这部分正是最不确定如何撰写的内容。即使一遍又一遍地阅读官方文档和示例。但是,正如您所说(并且为了安全起见),告知用户更改并没有错 :)
好吧,这是一篇优秀的文章
再观察一下。最好能检测到移动浏览器。各种自定义选择框的一个问题是,它们没有考虑到移动浏览器,移动浏览器有自己选择选项的原生方式(我猜它们更喜欢这种选择方式)
这就是
@media (hover: hover)
发挥作用的地方。它在所有主要/最新的浏览器中都能很好地工作。换句话说,没有一个移动浏览器匹配该媒体查询,这意味着自定义选择框在那里保持隐藏状态。在旧版浏览器中,也会显示原生选择框。我在三星 Note 8 上使用最新的 Chrome,获取的是自定义选择框而不是原生选择框。
它工作得还不错,我可以选择等,但我还是想提一下,因为它不是我预期的结果。
所以也许 Note 由于手写笔✍️♂️而支持悬停。
哇,@CanRu。带笔的平板手机一直是一个棘手的情况。如果您不介意,我想就该元素再问几个可用性问题。您可以在 Twitter 上联系我 @a_sandrina_p 或通过电子邮件[email protected]
Jeremy:
感谢您的深入探讨。键盘用户也应该获得完整的体验,例如从带有旗帜的国家/地区列表中进行选择。致敬。
多么令人惊叹的优雅解决方案,我真的很喜欢这个想法,感谢分享。
不过,我有一个疑问:对于那些有时使用鼠标,有时使用键盘的用户来说,这种变化是否会令人困惑,具体取决于表单的长度。不一定是残障人士(但也可能是),但我与许多 B2B 用户合作,许多用户都处于这种情况:有时是鼠标用户,有时是键盘用户,具体取决于他们在界面中的位置。如果您进行更多测试,这也可能是一个有趣的想法。
通常,您会尝试确保 UI 组件对用户来说是“可预测的”。而这可能会变得有点“不可预测”(因为它不是标准的)。所以,我只是很好奇他们的反应。(很想实现这一点自己进行测试,但我的开发人员永远不想为此调整我们的 React Material UI,所以目前是全天候使用原生元素)。
这似乎有很多非常紧密耦合的 css/HTML/JS,最终只对一部分用户有利。
尽管它仍然非常酷。
所有可访问性工作都是如此。实施起来很麻烦,很容易被忽略。一个盲人曾经向我展示了她如何使用网络。她是我认识的唯一一个盲人(根据你的评论,在制作网站时我可以忽略她),但我希望把她包括在内,让她能够使用原本会把她挡在门外的东西。
如果我在卡片上设置了 overflow hidden 会怎样?如何处理这种情况?我认为应该在页面主体末尾之前渲染自定义选择的下拉部分。我最近构建了这样一个选择框,但没有找到任何针对此问题的好的解决方案(在我的情况下来自 Vue 组件)。
这是一个棘手的极端情况。我会利用一些花哨的现代工具功能,比如“React Portals”,并在页面主体末尾创建列表,就像您建议的那样。
在演示中,从自定义选择框中跳出 Tab 键不起作用。它将焦点放在后面的电子邮件链接上,但选择框仍然处于展开状态。我在 React 中很难做到这一点。
Doug 说得对,这是当前实现的局限性。可能的解决方案:当自定义选择框打开时,为 Tab 键添加一个事件侦听器。当按下时,关闭选择框并删除事件侦听器。
看起来是一个完美的解决方案!非常酷。
但我不知道如何在 Angular 组件中实现它。有没有人在 Angular 项目中使用过它?
为了使它在从右到左的上下文中正确工作,我不得不修改一些 CSS 声明。具体来说,我不得不将 CSS 声明从
更改为(使用 CSS“逻辑”属性)