使用 base-select 为下拉选择框设置样式
使用appearance: base-select通过CSS美化原生select下拉菜单,了解弹出列表、选项、图标、无障碍支持、浏览器兼容性与限制。
在 <select> 及其 ::picker(select) 伪元素上设置 appearance: base-select 后,即可用 CSS 为整个控件设置样式,包括按钮、下拉列表、箭头、勾选标记以及每一个选项。
许多团队之所以维护一个自定义下拉组件,原因只有一个:原生 <select> 无法通过样式匹配设计稿。键盘交互、焦点管理和 ARIA 关联则是随之而来、不得不额外承担的工作。appearance: base-select 消除了这个理由。
本指南将完整介绍可定制的样式范围,从启用该特性到富内容选项,并说明随之需要做出的取舍。关于经典的 appearance: none 技巧,使用现代 CSS 为 select 元素设置样式一文提供了更全面的介绍,但对 base-select 只是一笔带过。更早期的方案可参阅无需 JavaScript 为 select 下拉框设置样式。
要点速览
appearance: base-select必须同时设置在select和::picker(select)上。仅在 picker 上设置不会产生任何效果。- picker 是位于顶层(top layer)的 popover,通过隐式锚点引用锚定到 select 按钮上,因此无需
z-index、portal 或anchor-name。 <selectedcontent>应放在作为<select>第一个子元素的<button>内,用于在收起状态的控件中显示所选选项的克隆内容。- 每个选项都应包含文本,因为纯图标选项无法为屏幕阅读器提供可朗读的内容,并且在不支持该特性的浏览器中会显示为空白。
- 使用 base-select 将失去以下能力:移动端操作系统原生选择器、在浏览器视口之外渲染,以及根据最长选项自动调整宽度。
可以删掉的组件
自定义 select 组件通常只是为了修正原生 <select> 元素的外观而存在,但在此过程中却不得不承担大量交互行为。Chrome 团队的发布文章指出,从 Chrome 135 开始,可以用 CSS 为 <select> 元素设置样式,同时保留内置控件的无障碍特性和标准行为。WebKit 随后在 Safari 27.0 中跟进,真正的 <select> 现在可以从头到尾重新设置样式。一旦浏览器支持该特性,保留基于 div 的 listbox 的主要理由便不复存在,为支撑它而重新实现的那些交互行为也随之可以舍弃。
如何启用 appearance: base-select?
要启用该特性,需要将 appearance: base-select 同时应用于 <select> 及其 picker。MDN 的可定制 select 指南解释说,select 可以单独启用,但 picker 只有在 select 也启用后才会生效。WebKit 关于可定制 select 的指导建议将这些增强样式包裹在 @supports (appearance: base-select) 中。后续所有规则都应放在同一个代码块内,这样不支持该特性的浏览器会显示普通的 select。
@supports (appearance: base-select) {
select,
::picker(select) {
appearance: base-select;
}
/* all following rules go here */
}
| 部位 | 选择器 | 说明 |
|---|---|---|
| 收起状态的控件 | select | select 按钮 |
| 箭头 | ::picker-icon | 不在无障碍树中 |
| 展开状态 | :open | picker 显示期间匹配 |
| 下拉列表 | ::picker(select) | 除第一个子元素按钮外的所有内容 |
| 每个选项 | option | 默认为 display: flex |
| 已选选项 | option:checked | |
| 选中标记 | ::checkmark | 不在无障碍树中 |
| 收起状态的内容 | selectedcontent | 所选选项的克隆 |
为收起状态的控件设置样式
收起状态的控件就是 select 元素本身。箭头是 ::picker-icon,而 :open 会在 picker 显示期间匹配,因此可以在 select 展开时旋转箭头。
select {
border: 2px solid #d4d4d8;
background: #f4f4f5;
padding: 10px;
transition: 0.4s;
}
select:hover,
select:focus {
background: #e4e4e7;
}
select::picker-icon {
color: #71717a;
transition: 0.4s rotate;
}
select:open::picker-icon {
rotate: 180deg;
}
作用于 rotate 的 transition 让箭头在 picker 每次展开和收起时都能平滑转动。
为展开的列表设置样式
可定制 select 展开后的下拉列表是 ::picker(select)。浏览器会将 <select> 内除第一个按钮之外的所有内容归入其中。选项默认是 flex 容器。:checked 匹配已选中的选项,::checkmark 则匹配其旁边的勾选标记。
::picker(select) {
border: none;
border-radius: 8px;
}
option {
display: flex;
gap: 16px;
padding: 10px;
}
option:hover,
option:focus {
background: #ede9fe;
}
option:checked {
font-weight: bold;
}
option::checkmark {
order: 1;
margin-left: auto;
content: "✔";
}
设置 order: 1 并配合 margin-left: auto,可以将勾选标记从行首移到行尾。
定位与动画
在可定制 select 中,下拉列表是一个在顶层打开的 popover。浏览器会自动通过 CSS 锚点定位将其与 select 按钮关联,因此无需 z-index、portal 或任何 anchor-name 配置。按钮与 picker 之间的关系,与 invoker commands 指南中所描述的按钮与 popover 之间的 invoker/popover 关系相同。默认样式还会在 picker 超出屏幕时将其移至另一侧,你也可以使用 anchor() 自行设置位置:
::picker(select) {
top: calc(anchor(bottom) + 1px);
left: anchor(10%);
}
要实现 picker 的淡入淡出效果,可以在其 popover 状态之间设置过渡:
::picker(select) {
opacity: 0;
transition: all 0.4s allow-discrete;
}
:open::picker(select) {
opacity: 1;
}
@starting-style {
:open::picker(select) {
opacity: 0;
}
}
allow-discrete 让离散属性 display 和 overlay 也能参与过渡。如果没有它,picker 会在淡出效果显现之前就离开顶层。@starting-style 用于设置淡入动画起始时的不透明度。
富内容选项与 selectedcontent
将 <selectedcontent> 放在作为 <select> 第一个子元素的 <button> 内。它会在收起状态的控件中显示所选选项内容的克隆,并且可以独立于下拉列表中的选项单独设置样式。选项现在可以包含图标、图片等标记内容。
<label for="status">Status</label>
<select id="status" name="status">
<button>
<selectedcontent></selectedcontent>
</button>
<option value="draft">
<span class="icon" aria-hidden="true">📝</span>
<span class="option-label">Draft</span>
</option>
<option value="review">
<img class="icon" src="review.svg" alt="">
<span class="option-label">In review</span>
</option>
<option value="published">
<span class="icon" aria-hidden="true">✅</span>
<span class="option-label">Published</span>
</option>
</select>
MDN 指出,图标可能会使收起状态的按钮变高,并导致箭头错位。可以仅在收起状态下隐藏图标:
selectedcontent .icon {
display: none;
}
展开的 picker 中的选项仍会保留图标。select 按钮是惰性(inert)的,因此放在其中的链接或按钮无法单独获得焦点或被点击,整体表现为一个按钮。MDN 还指出,按钮中的额外内容可能会改变屏幕阅读器所报告的 select 值,因此按钮内应只放置 <selectedcontent>。
浏览器为你处理了哪些工作?
使用 base-select 后,浏览器会处理自定义下拉组件必须自行重新实现的那些工作:键盘交互、焦点、顶层层叠、表单提交时的取值,以及屏幕阅读器语义。WebKit 表示,由于它仍是浏览器自身的控件,键盘支持和无障碍特性都是内置的。回放基于 div 的下拉组件上的用户会话,往往会暴露出键盘和焦点方面的 bug,而原生 select 从设计上就规避了这些问题。若想了解手动实现需要多少代码,可以参阅这篇手动构建无障碍下拉选择框的指南。
仍需遵循两条规则:
- 每个选项都要包含文本。 WebKit 的首要规则是:每个选项都需要真实的文本,或可访问的文本属性。视觉上隐藏的文本仍会被屏幕阅读器读出。装饰性图片应使用
alt="",由文本标签承载含义。 - 将自定义图标视为装饰。 屏幕阅读器会忽略通过
::checkmark和::picker-icon的content添加的任何内容,因此自定义的箭头或勾选标记永远不会被朗读。
现有读取 select 值的 JavaScript 代码仍可正常工作。如果选项没有 value 属性,select 的值将是所选选项去除首尾空白后的 textContent,因此当选项包含富内容标记时,务必设置 value。
兼容性、降级方案与取舍
appearance: base-select 已在 Chrome 135 及更高版本、Edge 等其他基于 Chromium 的浏览器以及 Safari 27.0 中发布。Firefox 已开始通过偏好设置加入该特性:appearance: base-select 值已在 Firefox 149 中以 flag 形式落地,但默认未开启。MDN 仍将其标记为有限可用(limited availability),请查阅 MDN 参考页面中的兼容性表格。Safari 自带一套默认的 base-select 样式,因此未设置样式的控件在不同浏览器中的外观并不一致。
不支持 base-select 的浏览器会忽略 <button> 和 <selectedcontent> 标记,渲染出一个可正常工作的普通 select,每个选项仅保留其文本。只有在选项包含文本的情况下,这种降级方案才有意义。
Chrome 的文章还列出了将会失去的三项行为:
- 展开的列表只能显示在浏览器窗口内。
- 在手机和平板上,显示的是浏览器的选择器,而不是操作系统原生的选择器。
- select 的宽度不再跟随最长的选项,因此需要自行在
select, ::picker(select)上设置width。
base-select 还改变了 HTML 解析器解析 <select> 内部内容的方式。MDN 警告,这可能与 JavaScript 框架产生冲突:有些框架会阻止这些特性,而在另一些框架中,服务端渲染可能会导致 hydration 出错。在移除现有组件之前,请先在你所用的框架中测试该特性。
总结
使用 appearance: base-select 的原生 select 让你掌控样式,同时由浏览器继续负责键盘、焦点、层叠和无障碍。要上手尝试,可以挑选一个简单的自定义下拉组件,在 @supports 代码块中重新构建它,确保每个选项都包含文本,并在不支持 base-select 的浏览器中进行检查。如果它在那里仍能作为普通 select 正常工作,且上述取舍可以接受,就可以移除这个自定义组件了。
常见问题
在 select 上使用 appearance: none 和 appearance: base-select 有什么区别?
appearance: none 只会去除收起状态按钮的原生样式。下拉列表及其选项仍然使用浏览器或操作系统的选择器,大多数 CSS 无法作用于它们。而同时设置在 select 和 ::picker(select) 上的 appearance: base-select,还允许你为 picker、选项、箭头和勾选标记设置样式,并允许在选项中使用富 HTML 内容。不支持该特性的浏览器会忽略它,继续使用经典控件。
如何在 JavaScript 中检测 base-select 支持?
调用 CSS.supports('appearance', 'base-select')。只有在能识别该值的浏览器中才会返回 true。它检测的内容与 CSS 条件 @supports (appearance: base-select) 相同,因此可以据此决定是否加载降级的下拉组件,或者添加一个用于启用自定义样式的类名。请使用特性检测而非 User Agent 嗅探,因为各个引擎加入支持的时间各不相同。
base-select 是否适用于 select multiple 或 size 属性?
适用,但需处于 listbox 模式。设置 multiple 属性或大于 1 的 size 值时,选项会以页面上的列表框形式显示,而不是一个点击后弹出列表的按钮。从 Chrome 145 开始,base-select 也支持此模式,单选和多选均可。但它目前尚不支持同时设置 multiple 和 size=1 时出现的多选弹出列表。可定制的 listbox 没有 picker,因此 ::picker(select)、::picker-icon 和 selectedcontent 均不适用。其他引擎的支持情况请查阅 MDN 的兼容性表格。
Truly understand users experience
See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data.
Star on GitHub12k