纯 CSS 自定义样式单选按钮
这是系列文章的第十八篇,该系列文章探讨了过去 13 多年来我作为前端开发者所遇到的各种问题,并提出了相应的解决方案。访问ModernCSS.dev可以查看整个系列文章及其他资源。
通过组合以下属性,我们可以用纯 CSS 创建自定义、跨浏览器、可主题化、可扩展的单选按钮:
currentColor
主题能力em
相对尺寸单位radial-gradient
:before
与:checked
指标相比- CSS 网格布局用于对齐输入和标签
注意:这些样式中有很多与自定义复选框样式一集重叠,您可能有兴趣接下来阅读!
现已推出:我的 Egghead 视频课程:无障碍跨浏览器 CSS 表单样式。你将学习如何将本教程中描述的技术提升到一个新的水平,创建一个可主题化的表单设计系统,并扩展到你的各个项目。
单选按钮 HTML
在 HTML 中,有两种适当的方法来布局单选按钮。
第一个将 包裹input
在 内label
。这隐式地将标签与其标记的输入关联起来,并且还增加了选择单选按钮的点击区域。
<label>
<input type="radio" name="radio" />
Radio label text
</label>
第二种是让input
和label
成为兄弟并使用for
设置为无线电值的属性id
来创建关联。
<input type="radio" name="radio" id="radio1" />
<label for="radio1">Radio label text</label>
我们的技术适用于任一设置,但我们将选择包装标签方法来防止包含额外的 div。
我们的演示的基本 HTML 包括类和两个单选按钮(测试:checked
未选中状态所必需的),如下所示:
<label class="radio">
<span class="radio__input">
<input type="radio" name="radio">
<span class="radio__control"></span>
</span>
<span class="radio__label">Radio 1</span>
</label>
<label class="radio">
<span class="radio__input">
<input type="radio" name="radio">
<span class="radio__control"></span>
</span>
<span class="radio__label">Radio 2</span>
</label>
对于单选按钮组,也需要提供相同的name
属性。
Chrome 中的原生 HTML 元素显示方式如下:
原生单选按钮的常见问题
导致开发人员寻求单选按钮自定义样式解决方案的主要问题是它们在不同的浏览器之间外观存在差异,而当包括移动浏览器时,这种差异还会增大。
例如,以下是 Mac 版 Firefox(左)、Chrome(中)和 Safari(右)上显示的单选按钮:
第二个问题是原生单选按钮无法仅根据字体大小进行缩放。以下是这些浏览器中再次出现的故障,顺序相同:
我们的解决方案将实现以下目标:
- 与
font-size
提供的规模label
- 获得与标签相同的颜色,以方便主题化
- 实现一致的跨浏览器设计风格,包括
:focus
状态 - 保持键盘可访问性
主题变量和box-sizing
重置
在我们的级联中,有两个基本 CSS 规则必须放在第一位。
首先,我们创建一个名为的自定义变量,--color
我们将使用它作为一种简单的方法轻松地为单选按钮设置主题。
:root {
--color: rebeccapurple;
}
接下来,我们使用通用选择器重置box-sizing
所使用的方法border-box
。这意味着 padding 和 border 将包含在任何元素最终尺寸的计算中,而不是将计算尺寸增加到超出任何设定尺寸的范围。
*,
*:before,
*:after {
box-sizing: border-box;
}
标签样式
我们的标签使用了 类.radio
。我们这里要包含的基本样式是font-size
和color
。回想一下之前提到的,font-size
还不会对 radio 的视觉大小产生影响input
。
.radio {
font-size: 2.25rem;
color: var(--color);
}
我们使用异常大的尺寸font-size
只是为了强调教程演示中的视觉变化。
我们的标签也是我们设计的布局容器,我们将设置它以使用 CSS 网格布局来利用grid-gap
。
.radio {
// ...existing styles
display: grid;
grid-template-columns: min-content auto;
grid-gap: 0.5em;
}
这是我们在 Chrome 中捕获的进度,其中 Inspector 显示网格线:
自定义单选按钮样式
好的,这就是您来这里要看的部分!
为了做好准备,我们将我们的 包裹input
在span
类中radio__input
。然后,我们还添加了一个span
作为 的兄弟input
类radio__control
。
这里的顺序很重要,正如我们在为:checked
和设计样式时所看到的那样:focus
。
步骤 1:隐藏原生广播输入
我们需要隐藏原生的单选按钮输入,但保持其在技术上可访问,以实现正确的键盘交互并保持对:focus
状态的访问。
为了实现这一点,我们将使用opacity
来在视觉上隐藏它,并将其设置为width
和height
以0
减少其对元素流的影响。
.radio__input {
input {
opacity: 0;
width: 0;
height: 0;
}
}
您可能在过去见过更冗长的解决方案,但当我们添加自定义样式的控件时,我们就会明白为什么它有效。
步骤 2:自定义未选中的单选按钮样式
radio__control
对于我们的自定义收音机,我们将样式附加到输入之后的同级类的范围。
我们将其定义为块元素,并使用 来调整em
其大小,以使其与应用于标签的 保持相对关系font-size
。我们还使用em
的值border-width
来保持相对外观。Good oleborder-radius: 50%
通过将元素渲染为圆形来完成预期的外观。
.radio__control {
display: block;
width: 1em;
height: 1em;
border-radius: 50%;
border: 0.1em solid currentColor;
}
这是我们隐藏原生输入并为自定义单选按钮控件定义这些基本样式后的进展:
呃-这种排列是怎么回事?
尽管定义了width
和,height
但0
按照跨度的默认行为,它仍然被计算为具有维度的元素。
对此的快速修复是添加display: flex
到.radio__input
包装本机输入和自定义控件的跨度:
.radio__input {
display: flex;
}
Flex 尊重0
尺寸,自定义控件弹出并充当其中的唯一元素.radio__input
。
步骤 3:改进输入与标签对齐
如果您使用过网格或弹性框,那么您现在的本能可能是应用align-items: center
视觉调整输入相对于标签文本的对齐方式。
但是,如果标签太长,可能会断成多行怎么办?在这种情况下,沿水平中心对齐可能不太理想。
相反,让我们进行调整,以便输入相对于标签文本的第一行保持水平居中。
我们的第一步是调整line-height
课程的跨度.radio__label
。
.radio__label {
line-height: 1;
}
毫无疑问,使用的值1
在这里是一种快速修复方法,如果您的应用程序经常有多行无线电标签,那么这可能并不可取。
根据所使用的字体,这可能无法 100% 解决对齐问题,在这种情况下,您可能会受益于以下额外的调整。
在我们的自定义控件上,我们将使用它transform
来将元素向上微调。这是一个有点神奇的数字,但作为起点,这个值是应用边框大小的一半。
.radio__control {
// ...existing styles
transform: translateY(-0.05em);
}
这样,我们的对齐就完成了,并且对于单行和多行标签都可以使用:
第四步::checked
国家
我们使用opacity: 0
原生的单选输入来保持键盘交互以及点击/轻触交互的可访问性。
它还保留了:checked
使用 CSS 检测其状态的能力。
还记得我提到过顺序很重要吗?由于我们的自定义控件位于原生输入框之后,因此当原生控件处于🙌状态时,我们可以使用相邻的兄弟组合 - +
- 来设置自定义控件的样式。:checked
选项 1:使用radial-gradient
我们可以添加radial-gradient
一个经典的实心圆圈外观:
.radio__input {
// ...existing styles
input {
// ...existing styles
&:checked + .radio__control {
background: radial-gradient(currentcolor 50%, rgba(255, 0, 0, 0) 51%);
}
}
}
您可以根据自己的喜好调整渐变的停止点。
请注意,使用 来
rgba
定义透明颜色而不是关键字transparent
,因为transparent
在 Safari 中使用渐变时会将其解释为“透明黑色”👎
以下是结果的 gif:
由于
radial-gradient
应用为background
,因此如果使用删除 CSS 背景的默认打印机设置打印表单页面,则它将不可见。
选项 2:使用:before
另一种方法是在自定义控件上使用:before
呈现为圆形的子元素。
这种方法的优点是它也可以用于动画。
我们首先需要改变包装跨度的行为.radio__control
:
.radio__control {
display:grid;
place-items: center;
}
:before
这是将自定义控件的水平和垂直中心对齐的最快方法。
然后,我们创建:before
元素,包括过渡并使用变换将其隐藏scale(0)
:
input + .radio__control::before {
content: "";
width: .5em;
height: .5em;
box-shadow: inset .5em .5em currentColor;
border-radius: 50%;
transition: 180ms transform ease-in-out;
transform: scale(0);
}
使用 ofbox-shadow
而不是 ofbackground-color
将使无线电状态在打印时可见(h/t Alvaro Montoro)。
最后,当 为 时input
,:checked
我们通过 使其可见,并scale(1)
借助 获得漂亮的动画效果transition
:
input:checked + .radio__control::before {
transform: scale(1);
}
下面是使用动画元素的结果的 gif :before
:
第五步::focus
国家
对于:focus
状态,我们将使用双精度box-shadow
来利用currentColor
但确保基本自定义单选按钮和:focus
样式之间的区别。
再次,我们将使用相邻兄弟组合器:
.radio__input {
// ...existing styles
input {
// ...existing styles
&:focus + .radio__control {
box-shadow: 0 0 0 0.05em #fff, 0 0 0.15em 0.1em currentColor;
}
}
}
定义的顺序box-shadow
与其分层相对应,第一个定义等于“顶层”。这意味着在这个规则中,我们首先创建一个细白边框的外观,它出现在一个羽化阴影上方,该阴影的值取自currentColor
。
下面是一个 gif 来演示:focus
外观:
这样,自定义单选按钮的基本样式就完成了!🎉
实验:使用:focus-within
样式化标签文本
由于标签不是本机输入的兄弟,因此我们不能使用:focus
输入的状态来设置其样式。
即将推出的伪选择器是:focus-within
,其特点之一是它可以将样式应用于包含已获得焦点的元素的元素。
ModernCSS 一集还介绍了纯 CSS 可访问下拉导航菜单
:focus-within
。
目前,:focus-within
需要一个polyfill,因此以下样式应被视为增强功能,而不应被视为提供焦点视觉指示的唯一方法。
我们要做的第一个调整是添加transition
并减少opacity
:radio__label
.radio__label {
// ...existing styles
transition: 180ms all ease-in-out;
opacity: 0.8;
}
确保降低的不透明度仍然满足调色板的适当对比度。
:focus-within
然后,我们将通过在标签上添加规则( .radio
) 来测试焦点。这意味着,当原生输入框(它是子元素,因此位于标签“内部”)获得焦点时,我们可以在焦点处于活动状态时为标签内的任何元素设置样式。
因此,我们将使用 稍微增加标签文本的视觉大小scale()
,并重新提高不透明度。
.radio {
// ...existing styles
&:focus-within {
.radio__label {
transform: scale(1.05);
opacity: 1;
}
}
}
使用scale()
可以防止调整大小影响元素的流动并导致抖动。过渡效果使其更加流畅,如下图所示:
演示
以下是整个解决方案,第一个无线电演示:checked
使用的状态radial-gradient
,第二个无线电演示使用的状态:before
:
查看自定义复选框样式,了解如何将样式扩展到:disabled
状态,并了解如何使用 SVG 作为:checked
指示器。