可定制的 select 元素
本文解释了如何使用实验性浏览器功能创建完全自定义的<select>
元素。这包括完全控制select按钮、下拉选择器、箭头图标、当前选择的复选标记以及每个单独的<option>
元素的样式。
警告:本文中演示的CSS和HTML功能目前浏览器支持有限;请查看各个功能参考页面上的浏览器兼容性表格以获取更多详细信息。一些JavaScript框架会阻止这些功能;在其他框架中,它们在启用服务器端渲染(SSR)时会导致水合失败。
Background
传统上,自定义<select>
元素的外观和感觉一直很困难,因为它们包含在操作系统级别设置样式的内部组件,这些组件无法使用CSS进行定位。这包括下拉选择器、箭头图标等。
以前,除了使用自定义JavaScript库之外,最好的选择是在<select>
元素上设置appearance
值为none
,以去除一些OS级别的样式,然后使用CSS自定义可设置样式的部分。这种技术在高级表单样式中有所解释。
可定制的<select>
元素为这些问题提供了解决方案。它们允许您仅使用HTML和CSS构建如下所示的示例,这些示例在支持的浏览器中完全自定义。这包括<select>
和下拉选择器布局、配色方案、图标、字体、过渡、定位、指示选定图标的标记等。
此外,它们在现有功能的基础上提供了渐进增强,在不支持的浏览器中退回到“经典”select。
您将在下面的部分中找到如何构建此示例。
哪些功能构成了可定制的select?
您可以使用以下HTML和CSS功能构建可定制的<select>
元素
- 普通的
<select>
、<option>
和<optgroup>
元素。它们与“经典”select的功能相同,只是它们允许的内容类型更多。 - 一个
<button>
元素作为<select>
元素的第一个子元素包含在内,这在“经典”select中以前是不允许的。当它包含在内时,它会替换关闭的<select>
元素的默认“按钮”渲染。这通常被称为select按钮(因为它是您需要按下才能打开下拉选择器的按钮)。注意:select按钮默认是惰性的,因此如果其中包含交互式子元素(例如链接或按钮),它仍将被视为用于交互的单个按钮——例如,子项将不可聚焦或不可点击。
<selectedcontent>
元素可以可选地包含在<select>
元素的第一个子<button>
元素中,以在关闭的<select>
元素中显示当前选定的值。它包含当前选定的<option>
元素内容的克隆(在底层使用cloneNode()
创建)。::picker(select)
伪元素,它定位选择器的全部内容。这包括<select>
元素内除第一个子<button>
之外的所有元素。appearance
属性值base-select
,它使<select>
元素和::picker(select)
伪元素选择浏览器定义的自定义select的默认样式和行为。:open
伪类,它在选择器(::picker(select)
)打开时定位select按钮。::picker-icon
伪元素,它定位select按钮内的图标——当select关闭时指向下方的箭头。:checked
伪类,它定位当前选定的<option>
元素。::checkmark
伪元素,它定位放置在当前选定的<option>
元素内的复选标记,以提供视觉指示哪个被选中。
此外,<select>
元素及其下拉选择器会自动分配以下行为
- 它们具有调用者/弹出窗口关系,如弹出窗口API所指定,它提供了通过
:popover-open
伪类选择打开时选择器的能力。有关弹出窗口行为的更多详细信息,请参阅使用弹出窗口API。 - 它们具有隐式锚点引用,这意味着选择器通过CSS锚点定位自动与
<select>
元素关联。浏览器默认样式将选择器定位在相对于按钮(锚点)的位置,您可以按照定位相对于其锚点的元素中解释的方式自定义此位置。浏览器默认样式还定义了一些位置尝试回退,如果选择器有溢出视口的危险,则会重新定位它。位置尝试回退在处理溢出:尝试回退和条件隐藏中有所解释。
注意:您可以通过查看相关功能(如<selectedcontent>
、::picker(select)
和::checkmark
)的参考页面上的浏览器兼容性表格来检查可定制<select>
的浏览器支持情况。
让我们通过页面顶部显示的示例来查看所有上述功能的实际应用。
可定制的select标记
我们的示例是一个典型的<select>
菜单,允许您选择一只宠物。标记如下
<form>
<p>
<label for="pet-select">Select pet:</label>
<select id="pet-select">
<button>
<selectedcontent></selectedcontent>
</button>
<option value="">Please select a pet</option>
<option value="cat">
<span class="icon" aria-hidden="true">🐱</span
><span class="option-label">Cat</span>
</option>
<option value="dog">
<span class="icon" aria-hidden="true">🐶</span
><span class="option-label">Dog</span>
</option>
<option value="hamster">
<span class="icon" aria-hidden="true">🐹</span
><span class="option-label">Hamster</span>
</option>
<option value="chicken">
<span class="icon" aria-hidden="true">🐔</span
><span class="option-label">Chicken</span>
</option>
<option value="fish">
<span class="icon" aria-hidden="true">🐟</span
><span class="option-label">Fish</span>
</option>
<option value="snake">
<span class="icon" aria-hidden="true">🐍</span
><span class="option-label">Snake</span>
</option>
</select>
</p>
</form>
注意:图标上包含aria-hidden="true"
属性,以便它们对辅助技术隐藏,避免选项值被宣布两次(例如,“猫 猫”)。
示例标记与“经典”<select>
标记几乎相同,但有以下区别
-
<button><selectedcontent></selectedcontent></button>
结构表示select<button>
。添加<selectedcontent>
元素会导致浏览器将当前选定的<option>
克隆到按钮内,然后您可以为其提供自定义样式。如果此结构未包含在您的标记中,浏览器将退回到在默认按钮内渲染选定选项的文本,您将无法轻松对其进行样式设置。注意:您可以在
<button>
中包含任意内容,以在关闭的<select>
内渲染您想要的任何内容,但请注意这样做。您包含的内容可能会改变暴露给辅助技术的<select>
元素的辅助值。 -
<select>
的其余内容表示下拉选择器,通常仅限于表示选择器中不同选择的<option>
元素。您可以在选择器中包含其他内容,但不建议这样做。 -
传统上,
<option>
元素只能包含文本,但在可定制的select中,您可以包含其他标记结构,如图像、其他非交互式文本级语义元素等。您甚至可以使用::before
和::after
伪元素来包含其他内容,但请记住,这不会包含在可提交的值中。在我们的示例中,每个<option>
包含两个<span>
元素,分别包含一个图标和一个文本标签,允许它们各自独立地进行样式设置和定位。注意:由于
<option>
内容可以包含多级DOM子树,而不仅仅是文本节点,因此关于浏览器应如何通过JavaScript提取当前<select>
值存在规则。将检索选定的<option>
元素的textContent
属性值,对其运行trim()
,并将结果设置为<select>
值。
这种设计允许不支持的浏览器回退到经典的<select>
体验。<button><selectedcontent></selectedcontent></button>
结构将被完全忽略,非文本<option>
内容将被剥离,只留下文本节点内容,但结果仍将起作用。
选择自定义select渲染
要选择自定义select功能和最小的浏览器基础样式(并删除操作系统提供的样式),您的<select>
元素及其下拉选择器(由::picker(select)
伪元素表示)都需要设置appearance
值为base-select
select,
::picker(select) {
appearance: base-select;
}
您可以选择只对<select>
元素启用新功能,而将选择器保留默认的操作系统样式,但在大多数情况下,您会希望同时启用两者。您不能在不启用<select>
元素的情况下启用选择器。
完成此操作后,结果是一个非常简单的<select>
元素渲染
您现在可以随意对其进行样式设置。首先,<select>
元素设置了自定义的border
、background
(在:hover
或:focus
时会改变)和padding
值,以及一个transition
,以便背景变化平滑动画
select {
border: 2px solid #dddddd;
background: #eeeeee;
padding: 10px;
transition: 0.4s;
}
select:hover,
select:focus {
background: #dddddd;
}
选择器图标的样式
要为select按钮内的图标(当select关闭时指向下方的箭头)设置样式,您可以使用::picker-icon
伪元素对其进行定位。以下代码为图标提供自定义的color
和一个transition
,以便其rotate
属性的变化平滑动画
select::picker-icon {
color: #999999;
transition: 0.4s rotate;
}
接下来,::picker-icon
与:open
伪类结合使用——它只在下拉选择器打开时定位select按钮——以便在<select>
打开时为图标提供180deg
的rotate
值。
select:open::picker-icon {
rotate: 180deg;
}
让我们看看目前为止的工作——注意当<select>
打开和关闭时,选择器箭头如何平滑地旋转180度
下拉选择器的样式
下拉选择器可以使用::picker(select)
伪元素进行定位。如前所述,选择器包含<select>
元素内除按钮和<selectedcontent>
之外的所有内容。在我们的示例中,这意味着所有<option>
元素及其内容。
首先,选择器的默认黑色border
被移除
::picker(select) {
border: none;
}
现在对<option>
元素进行样式设置。它们使用flexbox进行布局,将它们全部对齐到flex容器的开头,并在每个元素之间包含20px
的gap
。每个<option>
还具有与<select>
相同的border
、background
、padding
和transition
,以提供一致的外观和感觉
option {
display: flex;
justify-content: flex-start;
gap: 20px;
border: 2px solid #dddddd;
background: #eeeeee;
padding: 10px;
transition: 0.4s;
}
注意:可定制的<select>
元素<option>
默认设置display: flex
,但为了说明正在发生的事情,它仍然包含在我们的样式表中。
接下来,使用:first-of-type
、:last-of-type
和:not()
伪类的组合,在顶部和底部<option>
元素上设置适当的border-radius
,并从所有<option>
元素(最后一个除外)中移除border-bottom
,以避免边框看起来混乱和双重。我们还在外部::picker(select)
容器上设置相同的border-radius
,这样如果我们在页面上设置不同的背景颜色,就不会出现丑陋的方形白色框。
option:first-of-type {
border-radius: 8px 8px 0 0;
}
option:last-of-type {
border-radius: 0 0 8px 8px;
}
::picker(select) {
border-radius: 8px;
}
option:not(option:last-of-type) {
border-bottom: none;
}
接下来,使用:nth-of-type(odd)
为奇数<option>
元素设置不同的background
颜色以实现斑马条纹,并在焦点和悬停时为<option>
元素设置不同的background
颜色,以在选择期间提供有用的视觉高亮
option:nth-of-type(odd) {
background: white;
}
option:hover,
option:focus {
background: plum;
}
最后,本节为<option>
图标(包含在带有icon
类的<span>
元素中)设置了更大的font-size
,使其更大,并使用text-box
属性删除了图标表情符号在块开始和块结束边缘的一些恼人的间距,使其与文本标签更好地对齐
option .icon {
font-size: 1.6rem;
text-box: trim-both cap alphabetic;
}
我们的示例现在渲染如下
调整select按钮内选定选项内容的样式
如果您从最近的几个实时示例中选择任何宠物选项,您会注意到一个问题——宠物图标导致select按钮的高度增加,这也改变了选择器图标的位置,并且选项图标和标签之间没有间距。
这可以通过在包含在<selectedcontent>
中时隐藏图标来解决,<selectedcontent>
表示选定<option>
的内容在select按钮内的显示方式。在我们的示例中,它使用display: none
隐藏
selectedcontent .icon {
display: none;
}
这不会影响<option>
内容在下拉选择器中显示时的样式。
当前选定选项的样式
要设置下拉选择器中当前选定<option>
的样式,您可以使用:checked
伪类进行定位。这用于将选定<option>
元素的font-weight
设置为bold
option:checked {
font-weight: bold;
}
当前选择复选标记的样式
您可能已经注意到,当您打开选择器进行选择时,当前选定的<option>
在其行开始端有一个复选标记。此复选标记可以使用::checkmark
伪元素进行定位。例如,您可能希望隐藏此复选标记(例如,通过display: none
)。
您也可以选择对其进行一些更有趣的操作——前面<option>
元素是使用flexbox水平布局的,flex项目对齐到行的开头。在下面的规则中,通过设置一个大于0
的order
值,并使用auto
的margin-left
值将其对齐到行的末尾,将复选标记从行的开头移动到末尾(参见对齐和自动边距)。
最后,将content
属性的值设置为不同的表情符号,以设置要显示的不同图标。
option::checkmark {
order: 1;
margin-left: auto;
content: "☑️";
}
注意:::checkmark
和::picker-icon
伪元素不包含在辅助功能树中,因此在它们上设置的任何生成的content
都不会被辅助技术宣布。您仍然应该确保您设置的任何新图标在视觉上都符合其预期目的。
让我们再次查看示例的渲染情况。最后三节更新后的状态如下
使用弹出窗口状态为选择器添加动画效果
可定制的<select>
元素的select button
和下拉选择器会自动获得调用者/弹出窗口关系,如使用弹出窗口API中所述。这为<select>
元素带来了许多优势;我们的示例利用了使用过渡在弹出窗口隐藏和显示状态之间进行动画的能力。:popover-open
伪类表示处于显示状态的弹出窗口。
本节将快速介绍该技术——阅读动画弹出窗口以获得更详细的描述。
首先,选择器使用::picker(select)
被选中,并被赋予0
的opacity
值和all 0.4s allow-discrete
的transition
值。这会导致当弹出窗口状态从隐藏变为显示时,所有更改值的属性都进行动画。
::picker(select) {
opacity: 0;
transition: all 0.4s allow-discrete;
}
过渡属性列表包括opacity
,但它还包括两个由浏览器默认样式设置的离散属性
display
-
当弹出窗口状态从隐藏变为显示时,
display
值从none
变为block
。这需要进行动画以确保其他过渡可见。 overlay
-
当弹出窗口状态从隐藏变为显示时,
overlay
值从none
变为auto
,以将其提升到顶层,然后在隐藏时再次恢复,以将其移除。这需要进行动画以确保在过渡完成之前延迟从顶层移除弹出窗口,从而确保过渡可见。
注意:需要allow-discrete
值才能启用离散属性动画。
接下来,在显示状态下使用::picker(select):popover-open
选择选择器,并将其opacity
值设置为1
——这是过渡的结束状态
::picker(select):popover-open {
opacity: 1;
}
最后,由于选择器在从display: none
过渡到使其可见的display
值时正在进行过渡,因此过渡的起始状态必须在@starting-style
块内指定
@starting-style {
::picker(select):popover-open {
opacity: 0;
}
}
这些规则协同工作,使选择器在<select>
打开和关闭时平滑地淡入淡出。
使用锚点定位选择器
可定制的<select>
元素的select按钮和下拉选择器具有隐式锚点引用,并且选择器通过CSS锚点定位自动与select按钮关联。这意味着不需要使用anchor-name
和position-anchor
属性进行显式关联。
此外,浏览器的默认样式提供了默认位置,您可以按照定位相对于其锚点的元素中解释的方式自定义此位置。
在我们的演示中,选择器的位置是相对于其锚点设置的,通过在其top
和left
属性值中使用anchor()
函数
::picker(select) {
top: calc(anchor(bottom) + 1px);
left: anchor(10%);
}
这导致选择器的顶部边缘始终定位在select按钮底部边缘下方1像素处,选择器的左边缘始终定位在select按钮左边缘的10%
宽度处。
注意:如果您想删除隐式锚点引用,以阻止选择器锚定到<select>
元素,您可以通过将选择器的position-anchor
属性设置为当前文档中不存在的锚点名称,例如--not-an-anchor-name
来实现。另请参阅删除锚点关联。
最终结果
经过最后两节,我们的<select>
的最终更新状态渲染如下
自定义其他经典select功能
以上各节涵盖了可定制select中所有可用的新功能,并展示了它如何与经典单行select以及弹出窗口和锚点定位等相关现代功能进行交互。还有一些上面未提及的其他<select>
元素功能;本节讨论它们目前如何与可定制select协同工作
<select multiple>
-
目前还没有为可定制
<select>
元素上的multiple
属性指定任何支持,但将来会对此进行研究。 <optgroup>
-
<optgroup>
元素的默认样式与经典<select>
元素相同——加粗且缩进少于所包含的选项。您需要确保对<optgroup>
元素进行样式设置,使其符合整体设计,并请记住它们将像常规HTML中预期的容器一样行事。在可定制的<select>
元素中,<legend>
元素被允许作为<optgroup>
的子元素,以提供易于定位和设置样式的标签。这会替换<optgroup>
元素的label
属性中设置的任何文本,并且它具有相同的语义。
接下来
在本模块的下一篇文章中,我们将探讨现代浏览器中可用于为不同状态的表单设置样式的不同UI伪类。