12k
All articles

使用 base-select 为下拉选择框设置样式

使用appearance: base-select通过CSS美化原生select下拉菜单,了解弹出列表、选项、图标、无障碍支持、浏览器兼容性与限制。

OpenReplay Team
OpenReplay Team
使用 base-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 */
}
部位选择器说明
收起状态的控件selectselect 按钮
箭头::picker-icon不在无障碍树中
展开状态:openpicker 显示期间匹配
下拉列表::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 的兼容性表格。

Digital experience platform

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

We use cookies to improve your experience. By using our site, you accept cookies.