使用纯CSS的自定义选择样式(详细指南)

6,249 阅读11分钟

现代CSS为我们提供了一系列的属性来实现自定义的选择样式,这些样式对于单个、多个和禁用的select 元素来说,在顶级浏览器中具有近乎相同的初始外观。

我们的解决方案将使用一些属性和技术:

  • clip-path 创建自定义的下拉箭头
  • 用CSS网格布局来对齐本地选择和箭头
  • 自定义CSS变量,用于灵活的样式设计
  • em 用于相对尺寸的单位

本地选择#的常见问题

就像所有的表单字段类型一样,<select> ,在不同的浏览器中,其初始外观也不尽相同。

从左到右,以下是<select> 在Firefox、Chrome和Safari中的初始外观。

initial native select appearance with no custom styling

差异包括框的大小、字体大小、行高,最突出的是下拉指示器的样式不同。

我们的目标是在这些浏览器中创建相同的初始外观,包括多选和禁用状态。

注意:下拉列表仍然是不可样式化的,所以一旦打开<select> ,它仍然会接受各个浏览器对option 列表的样式。这没关系--我们可以处理这个问题,以保留原生选择的自由可及性!

基础HTML#

我们将专注于一个单一的<select> 开始:

<label for="standard-select">Standard Select</label>
<div class="select">
  <select id="standard-select">
    <option value="Option 1">Option 1</option>
    <option value="Option 2">Option 2</option>
    <option value="Option 3">Option 3</option>
    <option value="Option 4">Option 4</option>
    <option value="Option 5">Option 5</option>
    <option value="Option length">
      Option that has too long of a value to fit
    </option>
  </select>
</div>

标签并不是我们样式设计的一部分,但它是作为一个一般的要求包括在内的,特别是for 属性的值与id 上的<select> 相同。

为了完成我们的自定义样式,我们在本教程中用一个额外的div包裹了本机的选择,其类别为select

重置和删除继承的样式

正如我所有的教程中所包含的现代最佳实践一样,我们首先添加以下重置:

*,
*::before,
*::after {
  box-sizing: border-box;
}

在此之后,我们可以开始为本地select ,并应用以下内容来恢复其外观的规则:

select {
  // A reset of styles, including removing the default dropdown arrow
  appearance: none;
  // Additional resets for further consistency
  background-color: transparent;
  border: none;
  padding: 0 1em 0 0;
  margin: 0;
  width: 100%;
  font-family: inherit;
  font-size: inherit;
  cursor: inherit;
  line-height: inherit;
}

虽然其中大多数可能是熟悉的,但奇怪的是,appearance 。这是一个不常使用的属性,你会注意到它并不完全是我们想要的支持,但在这个例子中,它主要为我们提供的是移除本地浏览器的下拉箭头。

注意:CodePen被设置为使用autoprefixer,它将添加所需的预固定版本的appearance 属性。你可能需要为你的项目专门设置,或手动添加它们。我的HTML / Sass Jumpstart包括自动前缀器,作为生产构建的一部分。

好消息是,如果你需要的话,我们可以再添加一条规则,以获得对低版本IE的箭头的移除:

select::-ms-expand {
  display: none;
}

这个提示是在Filament Group的优秀文章中发现的,该文章展示了创建选择样式的另一种方法

最后一部分是删除默认的outline 。别担心--我们稍后将为:focus 状态添加一个替代品!

select {
  // ...existing styles
  outline: none;

这就是我们的进展的gif图。你可以看到,现在在点击它之前,没有任何视觉指示,这是一个select

demo of interacting with the reset select

自定义选择框的样式

首先,让我们设置一些CSS变量。这将使我们的选择框可以灵活地重新着色,例如代表错误状态:

:root {
  --select-border: #777;
  --select-focus: blue;
  --select-arrow: var(--select-border);
}

可访问性说明:作为一个用户界面元素,选择框的边框与周围的表面颜色必须有3:1或更大的对比。

现在是创建自定义选择样式的时候了,我们将把它应用到我们的包装div.select

.select {
  width: 100%;
  min-width: 15ch;
  max-width: 30ch;
  border: 1px solid var(--select-border);
  border-radius: 0.25em;
  padding: 0.25em 0.5em;
  font-size: 1.25rem;
  cursor: pointer;
  line-height: 1.1;
  background-color: #fff;
  background-image: linear-gradient(to top, #f9f9f9, #fff 33%);
}

首先,我们设置一些宽度限制。min-widthmax-width 的值主要是为了这个演示,你可以根据你的使用情况选择放弃或改变它。

然后我们应用一些盒子模型的属性,包括border,border-radius, 和padding 。注意em 单位的使用,这将使这些属性与设定的font-size 成比例。

在重置样式中,我们将几个属性设置为inherit ,所以这里我们定义这些属性,包括font-size,cursor, 和line-height

最后,我们为它提供背景属性,包括一个梯度,以获得最轻微的维度。如果你删除了背景属性,那么这个选择将是透明的,并且会拾起页面的背景。这可能是可取的,但是要注意并测试其对对比度的影响。

这就是我们的进展:updated select now has a visually apparent box appearance

自定义选择下拉箭头

对于我们的下拉箭头,我们将使用最令人兴奋的现代CSS属性之一:clip-path

剪贴路径让我们可以通过 "剪贴 "大多数元素默认的方形和矩形来制作各种形状。我在最近重新设计的投资组合网站上使用了clip-path

clip-path 有更好的支持之前,替代方法包括:

  • background-image - 典型的png,稍微现代一点的是SVG
  • 一个内联的SVG作为附加元素
  • 使用边框技巧来创建一个三角形

SVG可能是最佳的解决方案,但是当它被用作background-image ,它就失去了像图标一样的能力,因为它不能在不完全重新定义的情况下改变其属性,如填充颜色。这意味着我们不能使用我们的CSS自定义变量。

将SVG放在内联中可以解决fill 颜色的问题,但是这意味着每次定义<select> ,都要多加入一个元素。

通过clip-path ,我们得到了一个清晰的、可扩展的箭头 "图形",感觉就像一个SVG,但它的好处是可以使用我们的自定义变量,并且包含在样式中而不是HTML标记中。

为了创建箭头,我们将把它定义为一个::after 伪元素:

.select::after {
  content: "";
  width: 0.8em;
  height: 0.5em;
  background-color: var(--select-arrow);
  clip-path: polygon(100% 0%, 0 0%, 50% 100%);
}

clip-path 的语法有点奇怪,由于它不是本文的重点,我推荐以下资源:

  • Colby Fayock在这个蛋头视频中用一个例子解释了这个语法。
  • Clippy是一个在线工具,它允许你选择一个形状并调整点,同时动态生成clip-path CSS。

如果你正在关注,你可能已经注意到,尽管定义了widthheight ,箭头还是没有出现。当检查时,它发现::after 实际上没有被允许它的宽度。

我们将通过更新我们的.select ,使用CSS网格布局来解决这个问题:

.select {
  // ...existing styles
  display: grid;
}

这可以让箭头出现,本质上是给它一个类似于 "block "的显示值:

clip-path arrow now appears below the native select

在这个阶段,我们可以验证我们确实已经创建了一个三角形。

为了解决对齐问题,我们将使用我最喜欢的CSS网格黑客(如果你读过这里的一些文章,你就会觉得这是个老问题!)。

旧的CSS解决方案。position: absolute 新的CSS解决方案。一个单一的grid-template-areas ,把它们都包含进去

首先,我们将定义我们的区域,然后定义select::after 都使用它。这个名字是根据它所创建的元素的范围来决定的,我们把它叫做 "选择",这样就很容易了:

.select {
  // ...existing styles
  grid-template-areas: "select";
}

select,
.select:after {
  grid-area: select;
}

这给了我们一个重叠的箭头,由于通过源顺序的堆叠上下文,这个箭头在本地选择之上:

preview of the updated arrow position above the native select

我们现在可以使用网格属性来最终确定每个元素的排列:

.select {
  // ...existing styles
  align-items: center;
}

.select:after {
  // ...existing styles
  justify-self: end;
}

Ta-da!

finished initial styles for the custom select

:focus

哦,对了--还记得我们是如何删除outline 的吗? 我们需要解决掉那个丢失的:focus 状态。

有一个即将到来的属性,我们可以使用,叫做:focus-within ,但目前最好还是包括一个polyfill。

在本教程中,我们将使用另一种方法,以达到相同的结果,只是有点重了。

不幸的是,这意味着我们需要在DOM中再添加一个元素。

在原生选择元素之后,作为.select 内的最后一个子元素,添加:

<span class="focus"></span>

为什么是在后面?因为这是一个纯粹的CSS解决方案,把它放在原生选择元素之后意味着我们可以通过使用相邻的兄弟姐妹选择器--+ ,在select 的焦点上改变它。

这使得我们可以创建以下规则:

select:focus + .focus {
  position: absolute;
  top: -1px;
  left: -1px;
  right: -1px;
  bottom: -1px;
  border: 2px solid var(--select-focus);
  border-radius: inherit;
}

你可能想知道为什么我们在刚刚学习了之前的grid-area 黑客之后又回到了position: absolute

原因是为了避免基于padding的重新计算调整。如果你自己尝试一下,你会发现即使将widthheight 设置为100%,仍然会使其位于padding之内。

position: absolute 做得最好的工作是匹配元素的大小。我们在每个方向上把它多拉一个像素,以确保它与边界属性重叠。

但是,我们还需要对.select ,以确保它是相对于我们的选择的--嗯,position: relative

.select {
  // ...existing styles
  position: relative;

这就是我们在Chrome中看到的自定义选择的全部内容:

gif demo of focusing and selecting an option in the custom select

多重选择 #

选择还有另一种形式,即允许用户选择一个以上的选项。从HTML的角度来看,这只是意味着添加multiple 属性,但我们也将添加一个类来帮助创建样式调整,称为select--multiple

<label for="multi-select">Multiple Select</label>
<div class="select select--multiple">
  <select id="multi-select" multiple>
    <option value="Option 1">Option 1</option>
    <option value="Option 2">Option 2</option>
    <option value="Option 3">Option 3</option>
    <option value="Option 4">Option 4</option>
    <option value="Option 5">Option 5</option>
    <option value="Option length">
      Option that has too long of a value to fit
    </option>
  </select>
  <span class="focus"></span>
</div>

看看它,我们可以看到它有利地继承了我们大部分的样式,除了我们在这个视图中不需要箭头:

multiple select with inherited styles as previously defined

这是一个快速修复,调整我们定义箭头的选择器。我们使用:not() 来排除我们新定义的类:

.select:not(.select--multiple)::after

我们对多选有几个小的调整,第一个是去掉之前为了给箭头腾出空间而添加的padding。

select[multiple] {
  padding-right: 0;
}

默认情况下,带有长值的选项会溢出可见区域并被剪掉,但我发现主要的浏览器都允许根据你的需要覆盖包装。

select[multiple] option {
  white-space: normal;
}

作为选择,我们可以在选择上设置一个height ,以带来更可靠的跨浏览器行为。通过测试,我了解到Chrome和Firefox会显示部分选项,但Safari会完全隐藏一个不能完全进入视野的选项。

高度必须直接在本机选择上设置。考虑到我们的其他样式,数值6rem ,将能够显示3个选项:

select[multiple] {
  // ...existing styles
  height: 6rem;
}

在这一点上,由于目前的浏览器支持,我们已经做了尽可能多的调整:

:selected options 的状态在Chrome中是相当可定制的,在Firefox中有些可定制,而在Safari中完全不可定制。请看CodePen的演示,有一个部分可以不加注释来预览。

:disabled 样式

虽然我主张不显示禁用的控件,但我们应该为这种状态准备样式,以覆盖我们的基础。

为了强调禁用状态,我们想应用灰色的背景。但是由于我们已经在.select ,而且没有一个:parent 选择器,我们需要创建最后一个类来处理这种状态:

.select--disabled {
  cursor: not-allowed;
  background-color: #eee;
  background-image: linear-gradient(to top, #ddd, #eee 33%);
}

在这里,我们更新了光标作为一个额外的提示,表明该字段不能被交互,并更新了我们之前设置的背景值为白色,现在对于禁用状态来说更多的是灰色。

这导致了以下的外观:

previous of the disabled state styles for both single and multiple select

演示

你可以自己测试一下,但这里是整个解决方案的预览,从左到右分别是Firefox、Chrome和Safari:

final styled selects across the browsers mentioned