效果演示
实现了一个单选按钮组,用户可以选择其中一个选项。当用户选择一个选项时,会出现一个选择框,选择框会从选项的上方滑入,并在选项上方显示一个白色的背景。当用户再次选择一个选项时,选择框会从选项的上方滑出,并在新的选项上方显示一个白色的背景。整个效果类似于 iOS 或 Android 系统中的单选按钮组。
Code
<main>
<form>
<label>
<input type="radio" name="inventor" value="bell">
<span class="truncate">Alexander Graham Bell</span>
</label>
<label>
<input type="radio" name="inventor" value="morse">
<span class="truncate">Samuel Morse</span>
</label>
<label>
<input type="radio" name="inventor" value="edison">
<span class="truncate">Thomas Edison</span>
</label>
<div class="selection"></div>
</form>
</main>
* {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--hue: 223;
--bg: hsl(var(--hue), 90%, 90%);
--fg: hsl(var(--hue), 90%, 10%);
--primary: hsl(var(--hue), 90%, 50%);
--trans-dur: 0.3s;
--trans-timing: cubic-bezier(0.65, 0, 0.35, 1);
font-size: calc(16px + (24 - 16) * (100vw - 320px) / (2560 - 320));
}
body,
input {
color: var(--fg);
font: 1em/1.5 "DM Sans", sans-serif;
}
body {
background-color: var(--bg);
display: flex;
height: 100vh;
transition:
background-color var(--trans-dur),
color var(--trans-dur);
}
main {
margin: auto;
padding: 1.5em 0;
width: 100%;
}
form {
margin: auto;
max-width: 20em;
position: relative;
width: calc(100% - 3em);
}
label,
input[type="radio"] {
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
label {
background-color: hsla(0, 0%, 100%, 0);
border-radius: 0.75em;
display: flex;
align-items: center;
padding: 1.5em;
transition: background-color var(--trans-dur);
}
input[type="radio"] {
background-color: hsl(0, 0%, 100%);
border-radius: 50%;
box-shadow:
0 0 0 0.0625em hsl(var(--hue), 90%, 80%),
0 0.125em 0.125em 0.0625em hsla(var(--hue), 90%, 10%, 0.3);
flex-shrink: 0;
margin-inline-end: 1em;
outline: transparent;
position: relative;
width: 1.5em;
height: 1.5em;
transition:
background-color var(--trans-dur),
box-shadow var(--trans-dur);
-webkit-appearance: none;
appearance: none;
}
input[type="radio"]:before,
input[type="radio"]:after {
border-radius: 50%;
content: "";
display: block;
position: absolute;
transform: scale(0);
transition: transform var(--trans-dur) var(--trans-timing);
}
input[type="radio"]:before {
box-shadow:
0 0 0 0.5em var(--primary) inset,
0 0 0 0.0625em var(--primary);
top: -0.0625em;
left: -0.0625em;
width: calc(100% + 0.125em);
height: calc(100% + 0.125em);
}
input[type="radio"]:after {
background-color: hsl(0, 0%, 100%);
top: 25%;
left: 25%;
width: 50%;
height: 50%;
}
input[type="radio"]:checked:before,
input[type="radio"]:checked:after {
transform: scale(1);
}
label:has(input[type="radio"]:checked),
label:has(input[type="radio"]:not(:checked)):hover {
background-color: hsla(0, 0%, 100%, 0.5);
}
.selection {
border-radius: 0.75em;
box-shadow: 0 0 0 0.125em var(--primary) inset;
display: none;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4.5em;
transition: transform var(--trans-dur) var(--trans-timing);
}
label:has(input[type="radio"]:checked)~.selection {
animation: fade-in var(--trans-dur) var(--trans-timing);
display: inherit;
}
label:nth-of-type(2):has(input[type="radio"]:checked)~.selection {
transform: translateY(100%);
}
label:nth-of-type(3):has(input[type="radio"]:checked)~.selection {
transform: translateY(200%);
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
:root {
--bg: hsl(var(--hue), 90%, 10%);
--fg: hsl(var(--hue), 90%, 90%);
}
label {
background-color: hsla(var(--hue), 90%, 30%, 0);
}
label:has(input[type="radio"]:checked),
label:has(input[type="radio"]:not(:checked)):hover {
background-color: hsla(var(--hue), 90%, 30%, 0.5);
}
input[type="radio"] {
background-color: hsl(var(--hue), 90%, 30%);
box-shadow:
0 0 0 0.0625em hsl(var(--hue), 90%, 50%),
0 0.125em 0.125em 0.0625em hsla(var(--hue), 90%, 10%, 0.3);
}
}
/* Animations */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
实现思路拆分
* {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
这段代码是用来重置所有元素的默认样式的。具体来说,它将所有元素的边框、盒模型、外边距和内边距都设置为 0。
:root {
--hue: 223;
--bg: hsl(var(--hue), 90%, 90%);
--fg: hsl(var(--hue), 90%, 10%);
--primary: hsl(var(--hue), 90%, 50%);
--trans-dur: 0.3s;
--trans-timing: cubic-bezier(0.65, 0, 0.35, 1);
font-size: calc(16px + (24 - 16) * (100vw - 320px) / (2560 - 320));
}
这段代码定义了一些 CSS 变量,用于在整个文档中重复使用相同的颜色和过渡效果。具体来说,它定义了一个名为 --hue
的 CSS 变量,用于存储当前的色相值。然后,它定义了 --bg
、--fg
和 --primary
三个 CSS 变量,分别用于存储背景色、文本颜色和主要颜色。最后,它定义了一个名为 --trans-dur
的 CSS 变量,用于存储过渡效果的持续时间,以及一个名为 --trans-timing
的 CSS 变量,用于存储过渡效果的动画函数。
body,
input {
color: var(--fg);
font: 1em/1.5 "DM Sans", sans-serif;
}
这段代码定义了整个文档的文本颜色和字体。具体来说,它将 body
和 input
元素的文本颜色设置为 --fg
,字体设置为 1em/1.5 "DM Sans", sans-serif
。
body {
background-color: var(--bg);
display: flex;
height: 100vh;
transition:
background-color var(--trans-dur),
color var(--trans-dur);
}
这段代码定义了整个文档的背景色和高度。具体来说,它将 body
元素的背景色设置为 --bg
,并将 display
属性设置为 flex
,以便在文档中使用 flexbox 布局。它还定义了一个过渡效果,用于在背景色和文本颜色之间平滑地过渡。
main {
margin: auto;
padding: 1.5em 0;
width: 100%;
}
这段代码定义了 main
元素的样式。具体来说,它将 main
元素的水平居中,并将其上下内边距设置为 1.5em
,左右内边距设置为 0。它还将 main
元素的宽度设置为 100%。
form {
margin: auto;
max-width: 20em;
position: relative;
width: calc(100% - 3em);
}
这段代码定义了 form
元素的样式。具体来说,它将 form
元素的水平居中,并将其最大宽度设置为 20em
。它还将 form
元素的位置设置为相对定位,以便在文档中使用绝对定位。最后,它将 form
元素的宽度设置为文档宽度减去 3em
。
label,
input[type="radio"] {
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
这段代码定义了 label
和 input[type="radio"]
元素的样式。具体来说,它将 label
和 input[type="radio"]
元素的光标设置为指针,并将 -webkit-tap-highlight-color
属性设置为透明,以便在 iOS 上隐藏点击高亮效果。
label {
background-color: hsla(0, 0%, 100%, 0);
border-radius: 0.75em;
display: flex;
align-items: center;
padding: 1.5em;
transition: background-color var(--trans-dur);
}
这段代码定义了 label
元素的样式。具体来说,它将 label
元素的背景色设置为半透明的白色,边框半径设置为 0.75em
,文本垂直居中对齐,内边距设置为 1.5em
。它还定义了一个过渡效果,用于在鼠标悬停时平滑地过渡背景色。
input[type="radio"] {
background-color: hsl(0, 0%, 100%);
border-radius: 50%;
box-shadow:
0 0 0 0.0625em hsl(var(--hue), 90%, 80%),
0 0.125em 0.125em 0.0625em hsla(var(--hue), 90%, 10%, 0.3);
flex-shrink: 0;
margin-inline-end: 1em;
outline: transparent;
position: relative;
width: 1.5em;
height: 1.5em;
transition:
background-color var(--trans-dur),
box-shadow var(--trans-dur);
-webkit-appearance: none;
appearance: none;
}
这段代码定义了 input[type="radio"]
元素的样式。具体来说,它将 input[type="radio"]
元素的背景色设置为白色,边框半径设置为 50%
,阴影设置为两个半透明的黑色圆圈,并将 flex-shrink
属性设置为 0,以便在文档中使用 flexbox 布局。它还将 margin-inline-end
属性设置为 1em
,以便在 label
元素和 input[type="radio"]
元素之间添加一些间距。最后,它将 outline
属性设置为透明,以便在 iOS 上隐藏点击高亮效果。
input[type="radio"]:before,
input[type="radio"]:after {
border-radius: 50%;
content: "";
display: block;
position: absolute;
transform: scale(0);
transition: transform var(--trans-dur) var(--trans-timing);
}
这段代码定义了 input[type="radio"]:before
和 input[type="radio"]:after
元素的样式。具体来说,它们都是 input[type="radio"]
元素的伪元素,用于创建圆圈的外观。它们的 border-radius
属性设置为 50%
,content
属性设置为一个空字符串,display
属性设置为 block
,position
属性设置为绝对定位,transform
属性设置为缩放为 0,transition
属性设置为过渡效果。
input[type="radio"]:before {
box-shadow:
0 0 0 0.5em var(--primary) inset,
0 0 0 0.0625em var(--primary);
top: -0.0625em;
left: -0.0625em;
width: calc(100% + 0.125em);
height: calc(100% + 0.125em);
}
这段代码定义了 input[type="radio"]:before
元素的样式。具体来说,它将 input[type="radio"]:before
元素的阴影设置为两个半透明的 var(--primary)
颜色,并将其上下左右内边距设置为 -0.0625em
,以便在圆圈周围创建一些间距。它还将其宽度和高度设置为 calc(1000% + 0.125em)
,以便在圆圈周围创建一些间距。
input[type="radio"]:after {
background-color: hsl(0, 0%, 100%);
top: 25%;
left: 25%;
width: 50%;
height: 50%;
}
这段代码定义了 input[type="radio"]:after
元素的样式。具体来说,它将 input[type="radio"]:after
元素的背景色设置为白色,并将其上下左右内边距设置为 25%
,以便在圆圈内部居中对齐。它还将其宽度和高度设置为 50%
,以便在圆圈内部创建一个半径为 50%
的圆。
input[type="radio"]:checked:before,
input[type="radio"]:checked:after {
transform: scale(1);
}
这段代码定义了 input[type="radio"]:checked:before
和 input[type="radio"]:checked:after
元素的样式。具体来说,它们都是 input[type="radio"]
元素的伪元素,用于在选中时放大圆圈。它们的 transform
属性设置为缩放为 1,以便在选中时放大圆圈。
label:has(input[type="radio"]:checked),
label:has(input[type="radio"]:not(:checked)):hover {
background-color: hsla(0, 0%, 100%, 0.5);
}
这段代码定义了 label:has(input[type="radio"]:checked)
和 label:has(input[type="radio"]:not(:checked):hover)
元素的样式。具体来说,它们都是 label
元素的伪类选择器,用于在选中和悬停时改变背景色。它们的 background-color
属性设置为半透明的白色,以便在选中时高亮显示。
.selection {
border-radius: 0.75em;
box-shadow: 0 0 0 0.125em var(--primary) inset;
display: none;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4.5em;
transition: transform var(--trans-dur) var(--trans-timing);
}
这段代码定义了 .selection
元素的样式。具体来说,它是一个绝对定位的元素,用于显示选中选项的背景色。它将 border-radius
属性设置为 0.75em
,box-shadow
属性设置为一个半透明的 var(--primary)
颜色的黑色阴影,并将其位置设置为绝对定位,以便在文档中使用绝对定位。它还将 display
属性设置为 none
,以便在文档中隐藏它,并将 pointer-events
属性设置为 none
,以便在文档中禁用指针事件。它还将 width
和 height
属性设置为 100%
,以便覆盖整个文档,并将 transition
属性设置为过渡效果。
label:has(input[type="radio"]:checked)~.selection {
animation: fade-in var(--trans-dur) var(--trans-timing);
display: inherit;
}
这段代码定义了 label:has(input[type="radio"]:checked)~.selection
元素的样式。具体来说,它是一个伪类选择器,用于在选中选项时显示选择框。它将 animation
属性设置为一个名为 fade-in
的动画,用于在显示选择框时平滑地过渡。它还将 display
属性设置为 inherit
,以便在文档中显示选择框。
label:nth-of-type(2):has(input[type="radio"]:checked)~.selection {
transform: translateY(100%);
}
这段代码定义了 label:nth-of-type(2):has(input[type="radio"]:checked)~.selection
元素的样式。具体来说,它是一个伪类选择器,用于在选中第二个选项时将选择框向下移动。它将 transform
属性设置为 translateY(100%)
,以便将选择框向下移动。
label:nth-of-type(2):has(input[type="radio"]:checked)~.selection {
transform: translateY(100%);
}
这行代码选择了第二个 label
元素,并检查该元素是否包含一个选中的单选按钮。如果是,则选择该元素的后代元素 .selection
,并将其 translateY 属性设置为 100%,从而将选择框从选项的上方滑入。
label:nth-of-type(3):has(input[type="radio"]:checked)~.selection {
transform: translateY(200%);
}
这行代码选择了第三个 label
元素,并检查该元素是否包含一个选中的单选按钮。如果是,则选择该元素的后代元素 .selection
,并将其 translateY 属性设置为 200%,从而将选择框从选项的上方滑入。
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
这段代码定义了一个名为 .truncate
的类,用于限制文本的宽度并添加省略号。当文本超出限制宽度时,会自动添加省略号。
@media (prefers-color-scheme: dark) {
:root {
--bg: hsl(var(--hue), 90%, 10%);
--fg: hsl(var(--hue), 90%, 90%);
}
label {
background-color: hsla(var(--hue), 90%, 30%, 0);
}
label:has(input[type="radio"]:checked),
label:has(input[type="radio"]:not(:checked)):hover {
background-color: hsla(var(--hue), 90%, 30%, 0.5);
}
input[type="radio"] {
background-color: hsl(var(--hue), 90%, 30%);
box-shadow:
0 0 0 0.0625em hsl(var(--hue), 90%, 50%),
0 0.125em 0.125em 0.0625em hsla(var(--hue), 90%, 10%, 0.3);
}
}
这段代码定义了一个媒体查询,用于在用户的设备设置为深色模式时应用不同的样式。在深色模式下,背景颜色、文本颜色、单选按钮的背景颜色和阴影颜色会发生变化。
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
这段代码定义了一个名为 fade-in
的动画,用于将元素从透明度为 0 淡入到透明度为 1。在这个动画中,元素的 opacity 属性从 0 淡入到 1。