纯粹的CSS自定义样式的单选按钮(附代码示例)

1,007 阅读7分钟

使用以下属性的组合,我们可以在纯CSS中创建自定义的、跨浏览器的、有主题的、可扩展的单选按钮:

  • currentColor 主题性的
  • em 相对大小的单位
  • radial-gradient 与 ,用于 指示器:before :checked
  • CSS网格布局来对齐输入和标签

单选按钮HTML

在HTML中,有两种适当的方法来布局单选按钮。

第一种是将input 包裹在label 。这隐含地将标签与它所标注的输入相关联,同时也增加了选择单选的点击区域:

<label>
  <input type="radio" name="radio" />
  Radio label text
</label>

第二种是让inputlabel 成为兄弟姐妹,并使用for 属性设置为单选的id 的值来创建关联:

<input type="radio" name="radio" id="radio1" />
<label for="radio1">Radio label text</label>

我们的技术在任何一种设置下都可以工作,尽管我们将选择包装标签的方法,以防止包括一个额外的div。

我们的演示的基础HTML包括类和两个单选框--测试:checked 与未选中状态的必要条件--如下:

<label class="form-control">
  <input type="radio" name="radio" />
  Radio
</label>

<label class="form-control">
  <input type="radio" name="radio" />
  Radio - checked
</label>

对于分组的单选按钮,也有必要提供相同的name 属性。

下面是Chrome浏览器中原生HTML元素的显示方式:

native radio buttons in Chrome

原生单选按钮的常见问题

导致开发者为单选按钮寻求自定义样式解决方案的主要问题是它们在不同浏览器之间的外观差异,如果包括移动浏览器,这种差异会更大。

作为一个例子,下面是单选按钮在Mac版Firefox(左)、Chrome(中)和Safari(右)上的显示:

radio buttons in Firefox, Chrome, Safari

第二个问题是,本地的单选按钮不能仅仅通过字体大小来调整。下面是在这些浏览器中再次展示的这个故障,顺序相同:

radio buttons in Firefox, Chrome, Safari with no text scaling

我们的解决方案将实现以下目标:

  • 与提供给font-sizelabel
  • 获得与标签相同的颜色,以方便主题的实现
  • 实现一致的、跨浏览器的设计风格,包括:focus 状态
  • 保持键盘的可访问性

主题变量和box-sizing Reset

在我们的级联中,有两个基本的CSS规则必须首先放置。

首先,我们创建一个名为--color 的自定义变量,我们将用它作为一个简单的方法来轻松地给我们的单选按钮做主题:

:root {
  --form-control-color: rebeccapurple;
}

接下来,我们使用通用选择器将box-sizing 方法重置为border-box 。这意味着在计算任何元素的最终尺寸时,将包括padding和border,而不是增加超过任何设定尺寸的计算尺寸:

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

标签样式

我们的标签使用的是.radio 。我们在这里要包含的基本样式是font-sizecolor 。回顾一下前面的内容,font-size 还不会对单选题的视觉尺寸产生影响input

.form-control {
  font-family: system-ui, sans-serif;
  font-size: 2rem;
  font-weight: bold;
  line-height: 1.1;
}

我们使用一个异常大的font-size ,只是为了强调教程演示中的视觉变化。

我们的标签也是我们设计的布局容器,我们将把它设置为使用CSS网格布局来利用grid-gap

.form-control {
  font-family: system-ui, sans-serif;
  font-size: 2rem;
  font-weight: bold;
  line-height: 1.1;
  display: grid;
  grid-template-columns: 1em auto;
  gap: 0.5em;
}

下面是我们在Chrome浏览器中捕捉到的进度,检查器显示了网格线:

radio label with grid layout revealed

自定义单选按钮的样式

好了,这就是你来这里的目的了

为了做好准备,我们在span 中用类radio__input 包裹了我们的input 。然后,我们还添加了一个span ,作为input 的兄弟姐妹,其类别为radio__control

这里的顺序很重要,我们将在为:checked:focus 样式时看到。

第1步:隐藏原生的单选输入法

我们需要隐藏本地的单选输入,但在技术上要保持它的可访问性,以实现适当的键盘交互,同时也要保持对:focus 状态的访问。

为了达到这个目的,我们将使用opacity ,在视觉上隐藏它,并将其widthheight 设置为0 ,以减少它对元素流的影响。

input[type="radio"] {
  /* Add if not using autoprefixer */
  -webkit-appearance: none;
  appearance: none;
  /* For iOS < 15 to remove gradient background */
  background-color: #fff;
  /* Not removed via appearance */
  margin: 0;
}

你可能在过去看到过更繁琐的解决方案,但当我们添加自定义风格的控件时,我们会看到为什么这样做是有效的。

第2步:自定义无勾选单选框样式

对于我们的自定义单选,我们将把样式附加到类radio__control 的span上,它是输入后的同级元素。

我们将把它定义为块状元素,使用em 来保持它与应用于标签的font-size 的相对位置。我们也用em 来表示border-width 的值,以保持相对的外观。良好的border-radius: 50% ,通过将该元素渲染成一个圆形来完成预期的外观。

input[type="radio"] {
  appearance: none;
  background-color: #fff;
  margin: 0;
  font: inherit;
  color: currentColor;
  width: 1.15em;
  height: 1.15em;
  border: 0.15em solid currentColor;
  border-radius: 50%;
}

.form-control + .form-control {
  margin-top: 1em;
}

这是我们在隐藏了本地输入并为自定义单选框定义了这些基本样式后的进展:

progress of styles for the custom radio control shows the custom control rendering lower than the radio label

呃--这个对齐方式是怎么回事?

尽管定义了widthheight 0 ,但在span的默认行为下,它仍然被计算为一个有尺寸的元素。

快速解决这个问题的方法是,将display: flex 添加到包裹本地输入和自定义控件的.radio__input span上。

.radio__input {  display: flex;}

Flex尊重0 的尺寸,而自定义控件则弹出并作为.radio__input 内的唯一元素。

result of adding display: flex to fix the alignment

第3步:改进输入与标签的对齐方式

如果你曾经使用过grid或flexbox,你现在的直觉可能是应用align-items: center ,对输入与标签文本的对齐进行光学调整。

但是,如果标签长到足以跨越多行,怎么办?在这种情况下,沿水平中心对齐可能是不可取的。

相反,让我们进行调整,使输入的内容与标签文本的第一行保持水平居中的关系。

我们的第一步是调整.radio__label 类跨度上的line-height

input[type="radio"] {
  appearance: none;
  background-color: #fff;
  margin: 0;
  font: inherit;
  color: currentColor;
  width: 1.15em;
  height: 1.15em;
  border: 0.15em solid currentColor;
  border-radius: 50%;
  transform: translateY(-0.075em);
}

使用1 的值确实是一个快速的解决方案,如果你的应用程序经常有多行的单选标签,那么它可能并不可取。

根据使用的字体,这可能无法100%解决对齐问题,在这种情况下,你可能会从下面的额外调整中受益。

在我们的自定义控件上,我们将使用transform ,使该元素向上移动。这是一个有点神奇的数字,但作为一个起点,这个值是应用边框的一半。

这样,我们的对齐方式就完成了,而且对单行和多行标签都有作用。

final alignment of input vs. label text

第四步::checked State

我们对opacity: 0 的使用保持了本地单选输入对键盘交互以及点击/点击交互的可访问性。

它还保持了用CSS检测其:checked 状态的能力。

还记得我提到的顺序问题吗?由于我们的自定义控件原生输入之后,我们可以使用相邻的兄弟姐妹组合--+ --在原生控件是:checked 🙌时为我们的自定义控件设置样式。

选项1:用创建圆圈radial-gradient

我们可以添加一个radial-gradient ,获得一个经典的填充圆的外观。

input[type="radio"] {
  /* ...existing styles */

  display: grid;
  place-content: center;
}

你可以根据你的喜好调整梯度的停止点。

注意使用rgba 来定义透明的颜色,而不是关键词transparent ,因为在Safari浏览器中使用transparent 的渐变有一个问题,它被解释为 "透明的黑色" 👎。

这是一张结果的gif图。

demo of the custom radio checked state with radial-gradient

由于radial-gradient 是作为background 应用的,所以如果用默认的打印机设置来打印表单页面,它将不可见,因为默认的打印机设置会删除 CSS 背景。

选项2:用以下方法创建圆圈:before

另一种方法是在自定义控件上使用:before ,成为显示为圆形的子元素。

这种方法的优点是,它也可以用来做动画。

我们首先需要改变.radio__control wrapping span的行为。

input[type="radio"]::before {
  content: "";
  width: 0.65em;
  height: 0.65em;
  border-radius: 50%;
  transform: scale(0);
  transition: 120ms transform ease-in-out;
  box-shadow: inset 1em 1em var(--form-control-color);
}

这是最快的方法,可以将:before 对准自定义控件的水平和垂直中心。

然后,我们创建:before 元素,包括一个过渡,并使用转换隐藏它与scale(0)

input[type="radio"] {
  /* ...existing styles */
  display: grid;
  place-content: center;
}

input[type="radio"]::before {
  content: "";
  width: 0.65em;
  height: 0.65em;
  border-radius: 50%;
  transform: scale(0);
  transition: 120ms transform ease-in-out;
  box-shadow: inset 1em 1em var(--form-control-color);
}

input[type="radio"]:checked::before {
  transform: scale(1);
}

使用box-shadow ,而不是background-color ,就可以在打印时看到无线电的状态(h/tAlvaro Montoro)。

最后,当input:checked ,我们用scale(1) 使其可见,由于transition ,有一个很好的动画效果。

input[type="radio"]::before {
  /* ...existing styles */

  /* Windows High Contrast Mode */
  background-color: CanvasText;
}

这里是一个使用动画的:before 元素的结果的gif。

demo of the custom radio checked state with :before

第5步::focus 状态

对于:focus 状态,我们将使用一个双重的box-shadow ,以便利用currentColor ,但确保基本的自定义单选按钮和:focus 风格之间的区别。

同样,我们将使用相邻的兄弟姐妹组合器。

input[type="radio"]:focus {
  outline: max(2px, 0.15em) solid currentColor;
  outline-offset: max(2px, 0.15em);
}

box-shadow 定义的顺序与它们的分层相对应,第一个定义等同于 "顶层"。这意味着在这个规则中,我们首先创建一个薄的白色边框的外观,它出现在一个羽毛状的阴影上面,这个阴影的值来自currentColor

这里有一个GIF来演示:focus 的外观。

demo of the custom radio focused state

就这样,一个自定义的单选按钮的基本样式就完成了!🎉

实验:使用:focus-within ,为标签文本样式

由于标签不是本地输入的兄弟姐妹,我们不能使用输入的:focus 状态来设计它。

一个即将推出的伪选择器是:focus-within ,它的一个特点是可以将样式应用于包含已获得焦点的元素。

目前,:focus-within 需要一个polyfill,所以下面的样式应该被认为是一种增强,而不是作为提供焦点的视觉指示的唯一方法来依赖。

我们要做的第一个调整是添加一个transition ,并降低opacityradio__label

确保减少后的不透明度仍然符合你的调色板的适当对比度。

然后,我们将通过在标签(.radio)上添加一个:focus-within 的规则来测试焦点。这意味着当本机输入--它是一个孩子,因此 "在 "标签内--收到焦点时,我们可以在焦点激活时为标签内的任何元素设置样式。

因此,我们将使用scale() ,稍微提高标签文本的视觉尺寸,并将不透明度调高:

.form-control:focus-within {
  color: var(--form-control-color);
}

使用scale() ,可以防止调整大小影响元素的流动和造成任何抖动。如同这个GIF中所看到的那样,这个过渡使之变得漂亮而平滑。

demo of custom radio focus-within state