简要介绍如何构建颜色自适应、响应迅速且可访问的 <button>
组件。
在这篇博文中,我想分享我对如何构建颜色自适应、响应迅速且可访问的 <button>
元素的看法。试用演示版并查看源代码
如果你更喜欢视频,可以参考本博文的 YouTube 版本:
概览
<button>
元素是为用户互动而构建的。它的 click
事件通过键盘、鼠标、触摸、语音等触发,并具有关于时间的智能规则。此外,每个浏览器还附带了一些默认样式,因此您可以直接使用它们,而无需进行任何自定义。此外,您还可以使用 color-scheme
选择启用浏览器提供的浅色按钮和深色按钮。
此外,还有不同类型的按钮,每种按钮都显示在前面的 Codepen 嵌入中。没有类型的 <button>
将调整为位于 <form>
内,并更改为提交类型。
<!-- buttons -->
<button></button>
<button type="submit"></button>
<button type="button"></button>
<button type="reset"></button>
<!-- button state -->
<button disabled></button>
<!-- input buttons -->
<input type="button" />
<input type="file">
在本月的 GUI 挑战中,每个按钮都将获得一些样式,有助于在视觉上区分其意图。重置按钮具有破坏性,因此带有警告颜色;提交按钮会显示蓝色强调文本,因此看起来比常规按钮略有提升性。
按钮还有供 CSS 用于样式设置的伪类。这些类提供了用于自定义按钮感觉的 CSS 钩子::hover
适用于鼠标在按钮上时,:active
适用于按下鼠标或键盘时,而 :focus
或 :focus-visible
适用于辅助技术样式设置。
button:hover {}
button:active {}
button:focus {}
button:focus-visible {}
Markup
除了 HTML 规范提供的按钮类型之外,我还添加了一个带有图标的按钮和一个具有自定义类 btn-custom
的按钮。
<button>Default</button>
<input type="button" value="<input>"/>
<button>
<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<path d="..." />
</svg>
Icon
</button>
<button type="submit">Submit</button>
<button type="button">Type Button</button>
<button type="reset">Reset</button>
<button disabled>Disabled</button>
<button class="btn-custom">Custom</button>
<input type="file">
然后,为了进行测试,每个按钮都被放入表单中。这样,我可以确保为充当提交按钮的默认按钮适当更新样式。我还更改图标策略(从内嵌 SVG 改为遮盖 SVG),确保两者都能同样良好地工作。
<form>
<button>Default</button>
<input type="button" value="<input>"/>
<button>Icon <span data-icon="cloud"></span></button>
<button type="submit">Submit</button>
<button type="button">Type Button</button>
<button type="reset">Reset</button>
<button disabled>Disabled</button>
<button class="btn-custom btn-large" type="button">Large Custom</button>
<input type="file">
</form>
此时,组合矩阵非常难以处理。在按钮类型、伪类之间,以及是否属于某个形式中,按钮的组合超过 20 种。很棒的一点是,CSS 可帮助我们清晰地阐明每个元素!
无障碍功能
按钮元素自然可访问,但有几个常见的增强功能。
悬停并聚焦
我想使用 :is()
功能伪选择器将 :hover
和 :focus
组合在一起。这有助于确保我的界面始终考虑键盘和辅助技术的样式。
button:is(:hover, :focus) {
…
}
互动对焦环
我喜欢为键盘和辅助技术用户设置聚焦环的动画效果。为此,我将动画效果设置为距离按钮 5 像素,但前提是该按钮未处于活动状态。这样会产生一种效果,使聚焦环在被按下时缩小到按钮大小。
:where(button, input):where(:not(:active)):focus-visible {
outline-offset: 5px;
}
确保通过的色彩对比度
至少有四种不同的颜色组合(浅色和深色)需要考虑色彩对比度:按钮、提交按钮、重置按钮和已停用按钮。在这里,VisBug 用于一次性检查和显示其所有得分:
向看不到的人员隐藏图标
创建图标按钮时,图标应为按钮文本提供视觉支持。这也意味着,对于视力失明的用户来说,该图标并无价值。幸运的是,浏览器提供了一种方式来让屏幕阅读器技术隐藏内容,从而让视力不佳的用户免受装饰性按钮图片的干扰:
<button>
<svg … aria-hidden="true">...</svg>
Icon Button
</button>
风格
在下一部分,我将首先创建一个自定义属性系统,用于管理按钮的自适应样式。借助这些自定义属性 我可以开始选择元素并自定义其外观
自适应自定义属性策略
此 GUI 挑战中使用的自定义属性策略与构建配色方案中使用的策略非常相似。对于自适应浅色和深色系统,系统会为每个主题定义一个自定义属性,并相应地为其命名。然后,使用单个自定义属性来保存主题的当前值,并将其分配给 CSS 属性。之后,可以将单个自定义属性更新为其他值,然后更新按钮样式。
button {
--_bg-light: white;
--_bg-dark: black;
--_bg: var(--_bg-light);
background-color: var(--_bg);
}
@media (prefers-color-scheme: dark) {
button {
--_bg: var(--_bg-dark);
}
}
我喜欢浅色主题和深色主题具有声明性且清晰明了。间接和抽象被分流到 --_bg
自定义属性,该属性现在是唯一的“响应式”属性;--_bg-light
和 --_bg-dark
是静态的。此外,很明显,浅色主题是默认主题,深色只是有条件地应用。
为实现设计一致性做好准备
共享选择器
以下选择器用于定位所有各种类型的按钮,一开始有点不堪重负。使用 :where()
,因此自定义按钮无需具体说明。按钮通常适用于替代场景,:where()
选择器可确保任务轻松完成。在 :where()
内,系统会选择每种按钮类型,包括 ::file-selector-button
(无法在 :is()
或 :where()
内使用)。
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"]
),
:where(input[type="file"])::file-selector-button {
…
}
所有自定义属性的范围都将在此选择器中。现在该查看所有自定义属性了!此按钮中使用了很多自定义属性。我会在此过程中介绍每个组,然后在本部分的末尾分享深色和减少动作的上下文。
按钮强调色
提交按钮和图标是增添色彩的好地方:
--_accent-light: hsl(210 100% 40%);
--_accent-dark: hsl(210 50% 70%);
--_accent: var(--_accent-light);
按钮文字颜色
按钮文本颜色不是白色或黑色,而是使用 hsl()
并遵循色调 210
的 --_accent
调暗或调亮版本:
--_text-light: hsl(210 10% 30%);
--_text-dark: hsl(210 5% 95%);
--_text: var(--_text-light);
按钮背景色
按钮背景遵循相同的 hsl()
模式,但浅色主题按钮除外,这些按钮设置为白色,因此其 Surface 使其看起来靠近用户或在其他 Surface 的前面:
--_bg-light: hsl(0 0% 100%);
--_bg-dark: hsl(210 9% 31%);
--_bg: var(--_bg-light);
按钮背景
此背景颜色可让 Surface 显示在其他 surface 后面,对于文件输入的背景非常有用:
--_input-well-light: hsl(210 16% 87%);
--_input-well-dark: hsl(204 10% 10%);
--_input-well: var(--_input-well-light);
按钮内边距
按钮中文本周围的间距使用 ch
单位完成,该单位是相对于字体大小的相对长度。当大按钮能够直接提升 font-size
并且按钮按比例缩放时,这一点至关重要:
--_padding-inline: 1.75ch;
--_padding-block: .75ch;
按钮边框
按钮的边框半径会存储在自定义属性中,以便文件输入可以与其他按钮匹配。边框颜色遵循既定的自适应颜色系统:
--_border-radius: .5ch;
--_border-light: hsl(210 14% 89%);
--_border-dark: var(--_bg-dark);
--_border: var(--_border-light);
按钮悬停突出显示效果
这些属性确立了用于在互动时过渡的尺寸属性,并且突出显示颜色遵循自适应颜色系统。我们稍后会在这篇博文中介绍它们是如何相互作用的,但最终它们将用于实现 box-shadow
效果:
--_highlight-size: 0;
--_highlight-light: hsl(210 10% 71% / 25%);
--_highlight-dark: hsl(210 10% 5% / 25%);
--_highlight: var(--_highlight-light);
按钮文字阴影
每个按钮都有一个细微的文本阴影样式。这有助于文本位于按钮顶部,提高易读性并为演示文稿润色一层。
--_ink-shadow-light: 0 1px 0 var(--_border-light);
--_ink-shadow-dark: 0 1px 0 hsl(210 11% 15%);
--_ink-shadow: var(--_ink-shadow-light);
按钮图标
图标的大小是两个字符,这得益于相对长度的 ch
单位,这有助于图标根据按钮文本按比例缩放。图标颜色依赖于 --_accent-color
,适用于自适应和主题内颜色。
--_icon-size: 2ch;
--_icon-color: var(--_accent);
按钮阴影
为了让阴影能够正确地适应光和暗,它们需要同时改变颜色和不透明度。浅色主题阴影在暗淡并偏向于叠加的表面颜色时效果最佳。深色主题阴影需要颜色更深、更饱和,以便可以叠加颜色较深的表面颜色。
--_shadow-color-light: 220 3% 15%;
--_shadow-color-dark: 220 40% 2%;
--_shadow-color: var(--_shadow-color-light);
--_shadow-strength-light: 1%;
--_shadow-strength-dark: 25%;
--_shadow-strength: var(--_shadow-strength-light);
利用自适应颜色和强度,我可以组建两种阴影深度:
--_shadow-1: 0 1px 2px -1px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 9%));
--_shadow-2:
0 3px 5px -2px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 3%)),
0 7px 14px -5px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 5%));
此外,为了让按钮拥有略微 3D 的外观,1px
框阴影还会产生错觉:
--_shadow-depth-light: 0 1px var(--_border-light);
--_shadow-depth-dark: 0 1px var(--_bg-dark);
--_shadow-depth: var(--_shadow-depth-light);
按钮过渡
按照自适应颜色的模式,我创建了两个静态属性来保存设计系统选项:
--_transition-motion-reduce: ;
--_transition-motion-ok:
box-shadow 145ms ease,
outline-offset 145ms ease
;
--_transition: var(--_transition-motion-reduce);
选择器中的所有属性一并显示
:where( button, input[type="button"], input[type="submit"], input[type="reset"], input[type="file"] ), :where(input[type="file"])::file-selector-button { --_accent-light: hsl(210 100% 40%); --_accent-dark: hsl(210 50% 70%); --_accent: var(--_accent-light);--_text-light: hsl(210 10% 30%); --_text-dark: hsl(210 5% 95%); --_text: var(--_text-light);
--_bg-light: hsl(0 0% 100%); --_bg-dark: hsl(210 9% 31%); --_bg: var(--_bg-light);
--_input-well-light: hsl(210 16% 87%); --_input-well-dark: hsl(204 10% 10%); --_input-well: var(--_input-well-light);
--_padding-inline: 1.75ch; --_padding-block: .75ch;
--_border-radius: .5ch; --_border-light: hsl(210 14% 89%); --_border-dark: var(--_bg-dark); --_border: var(--_border-light);
--_highlight-size: 0; --_highlight-light: hsl(210 10% 71% / 25%); --_highlight-dark: hsl(210 10% 5% / 25%); --_highlight: var(--_highlight-light);
--_ink-shadow-light: 0 1px 0 hsl(210 14% 89%); --_ink-shadow-dark: 0 1px 0 hsl(210 11% 15%); --_ink-shadow: var(--_ink-shadow-light);
--_icon-size: 2ch; --_icon-color-light: var(--_accent-light); --_icon-color-dark: var(--_accent-dark); --_icon-color: var(--accent, var(--_icon-color-light));
--_shadow-color-light: 220 3% 15%; --_shadow-color-dark: 220 40% 2%; --_shadow-color: var(--_shadow-color-light); --_shadow-strength-light: 1%; --_shadow-strength-dark: 25%; --_shadow-strength: var(--_shadow-strength-light); --_shadow-1: 0 1px 2px -1px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 9%)); --_shadow-2: 0 3px 5px -2px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 3%)), 0 7px 14px -5px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 5%)) ;
--_shadow-depth-light: hsl(210 14% 89%); --_shadow-depth-dark: var(--_bg-dark); --_shadow-depth: var(--_shadow-depth-light);
--_transition-motion-reduce: ; --_transition-motion-ok: box-shadow 145ms ease, outline-offset 145ms ease ; --_transition: var(--_transition-motion-reduce); }
深色主题自适应
设置深色主题属性后,-light
和 -dark
静态属性模式的值会变得清晰:
@media (prefers-color-scheme: dark) {
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"]
),
:where(input[type="file"])::file-selector-button {
--_bg: var(--_bg-dark);
--_text: var(--_text-dark);
--_border: var(--_border-dark);
--_accent: var(--_accent-dark);
--_highlight: var(--_highlight-dark);
--_input-well: var(--_input-well-dark);
--_ink-shadow: var(--_ink-shadow-dark);
--_shadow-depth: var(--_shadow-depth-dark);
--_shadow-color: var(--_shadow-color-dark);
--_shadow-strength: var(--_shadow-strength-dark);
}
}
不仅如此,这些自定义按钮的消费者也可以使用基本属性,确信它们将根据用户偏好设置进行相应调整。
减少动作自适应
如果这位访问用户接受动作,请将 --_transition
分配给 var(--_transition-motion-ok)
:
@media (prefers-reduced-motion: no-preference) {
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"]
),
:where(input[type="file"])::file-selector-button {
--_transition: var(--_transition-motion-ok);
}
}
一些共享的样式
按钮和输入法需要将其字体设置为 inherit
,以便与页面字体的其余部分匹配;否则,浏览器将为其设置样式。这也适用于 letter-spacing
。将 line-height
设置为 1.5
会设置信箱大小,从而在文本上方和下方留出一些空间:
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"]
),
:where(input[type="file"])::file-selector-button {
/* …CSS variables */
font: inherit;
letter-spacing: inherit;
line-height: 1.5;
border-radius: var(--_border-radius);
}
设置按钮样式
选择器调整
选择器 input[type="file"]
不是输入的按钮部分,而伪元素 ::file-selector-button
则是,因此我已从列表中移除 input[type="file"]
:
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"]
),
:where(input[type="file"])::file-selector-button {
}
光标和触摸调整
首先,我将光标样式设为 pointer
样式,这有助于按钮向鼠标用户指明其为互动式。然后,我添加 touch-action: manipulation
以使点击无需等待并观察潜在的双击,使按钮感觉更快:
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
cursor: pointer;
touch-action: manipulation;
}
颜色和边框
接下来,我使用之前创建的一些自适应自定义属性,自定义字体大小、背景、文字和边框颜色:
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
…
font-size: var(--_size, 1rem);
font-weight: 700;
background: var(--_bg);
color: var(--_text);
border: 2px solid var(--_border);
}
阴影
这些按钮采用了很棒的技巧。text-shadow
可适应浅色和深色,让按钮文本呈现出令人愉悦的细微外观,恰好位于背景上方。对于 box-shadow
,分配了三种阴影。第一个是 --_shadow-2
,是一个常规的方框阴影。第二个阴影会欺骗眼睛,使按钮看起来有点倾斜。最后一个阴影适用于悬停突出显示,最初的大小为 0,但之后会提供一个大小并进行转换,使其看起来从按钮开始变大。
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
…
box-shadow:
var(--_shadow-2),
var(--_shadow-depth),
0 0 0 var(--_highlight-size) var(--_highlight)
;
text-shadow: var(--_ink-shadow);
}
布局
我为该按钮指定了 flexbox 布局,具体来说就是适合其内容的 inline-flex
布局。然后,将文本居中,并垂直和水平将子项与中心对齐。这将有助于图标和其他按钮元素正确对齐。
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
…
display: inline-flex;
justify-content: center;
align-items: center;
text-align: center;
}
间距
对于按钮间距,我使用了 gap
来防止同级项触摸及设置内边距的逻辑属性,以便按钮间距适用于所有文本布局。
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
…
gap: 1ch;
padding-block: var(--_padding-block);
padding-inline: var(--_padding-inline);
}
触控和鼠标用户体验
下一部分主要面向使用移动设备的触摸用户。第一个属性 user-select
用于所有用户;它可防止文本突出显示按钮文本。在触摸设备上,当用户点按并按住某个按钮且操作系统突出显示按钮的文本时,这种情况最明显。
我通常发现这不是内置应用中的按钮用户体验,因此我通过将 user-select
设置为 none 来停用它。点按突出显示颜色 (-webkit-tap-highlight-color
) 和操作系统上下文菜单 (-webkit-touch-callout
) 是其他以 Web 为中心的按钮功能,它们不符合一般按钮用户的预期,因此我也移除了这些功能。
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
…
user-select: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
}
转场效果
将自适应 --_transition
变量分配给了 transition 属性:
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
…
transition: var(--_transition);
}
悬停鼠标时,当用户没有主动按下时,调整阴影突出显示大小,使其呈现出美观的焦点外观,看起来在按钮内会不断扩大:
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
):where(:not(:active):hover) {
--_highlight-size: .5rem;
}
获得焦点时,增加距离按钮的焦点轮廓偏移量,同时呈现漂亮的焦点外观,使其看起来在按钮内部不断扩大:
:where(button, input):where(:not(:active)):focus-visible {
outline-offset: 5px;
}
图标
为了处理图标,该选择器针对直接 SVG 子元素或自定义属性 data-icon
的元素添加了一个 :where()
选择器。图标大小通过自定义属性使用内嵌和块逻辑属性进行设置。已设置描边颜色,并设置了 drop-shadow
以与 text-shadow
匹配。flex-shrink
设置为 0
,使图标永不挤压。最后,选择带线的图标,并在此处使用 fill: none
和 round
线帽和线联接分配这些样式:
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
) > :where(svg, [data-icon]) {
block-size: var(--_icon-size);
inline-size: var(--_icon-size);
stroke: var(--_icon-color);
filter: drop-shadow(var(--_ink-shadow));
flex-shrink: 0;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
自定义提交按钮
我希望提交按钮的外观略有提升,为此,我将按钮的文本颜色设为强调色:
:where(
[type="submit"],
form button:not([type],[disabled])
) {
--_text: var(--_accent);
}
自定义重置按钮
我希望重置按钮具有一些内置警告信号,以提醒用户其潜在的破坏行为。我还选择为浅色主题按钮设置样式,为浅色调比深色主题更多。通过更改适当的浅色或深色底层颜色来完成自定义,并且该按钮会更新样式:
:where([type="reset"]) {
--_border-light: hsl(0 100% 83%);
--_highlight-light: hsl(0 100% 89% / 20%);
--_text-light: hsl(0 80% 50%);
--_text-dark: hsl(0 100% 89%);
}
我还认为,焦点轮廓颜色与红色的强调色保持一致会非常好。文本颜色可将深红色转换为浅红色。我让轮廓颜色与关键字 currentColor
一致:
:where([type="reset"]):focus-visible {
outline-color: currentColor;
}
自定义停用的按钮
已停用按钮在试图降低已停用按钮的色彩对比度以致其显得不太活跃时,这种情况是很常见的。我测试了每种颜色集,确保它们通过测试,从而推动 HSL 亮度值直至在开发者工具或 VisBug 中传递得分。
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
)[disabled] {
--_bg: none;
--_text-light: hsl(210 7% 40%);
--_text-dark: hsl(210 11% 71%);
cursor: not-allowed;
box-shadow: var(--_shadow-1);
}
自定义文件输入按钮
文件输入按钮是 span 和按钮的容器。CSS 能够为输入容器以及嵌套按钮设置样式,但无法为 span 设置样式。为容器指定 max-inline-size
,使其不会超过所需的大小,而 inline-size: 100%
则允许自身缩小并适应比实际大小更小的容器。背景颜色被设置为比其他 surface 更深的自适应颜色,因此它位于文件选择器按钮的后面。
:where(input[type="file"]) {
inline-size: 100%;
max-inline-size: max-content;
background-color: var(--_input-well);
}
文件选择器按钮和输入类型按钮专门指定了 appearance: none
,用于移除浏览器提供的任何未被其他按钮样式覆盖的样式。
:where(input[type="button"]),
:where(input[type="file"])::file-selector-button {
appearance: none;
}
最后,向按钮的 inline-end
添加外边距,以将 span 文本推离按钮,从而创建一些空间。
:where(input[type="file"])::file-selector-button {
margin-inline-end: var(--_padding-inline);
}
特殊深色主题例外情况
对于对比度较高的文本,我为主要操作按钮设置了颜色更深的背景,使这些按钮的外观更加显眼。
@media (prefers-color-scheme: dark) {
:where(
[type="submit"],
[type="reset"],
[disabled],
form button:not([type="button"])
) {
--_bg: var(--_input-well);
}
}
创建变体
为了有趣又实用,我选择展示如何创建一些变体。其中一个变体非常鲜艳,与主要按钮的外观类似。另一个变体过大。最后一个变体具有渐变色填充图标。
鲜艳按钮
为了实现此按钮样式,我直接用蓝色替换基础属性。虽然这一操作既快速又简单,但它移除了自适应属性,并且在浅色主题和深色主题下看起来都一样。
.btn-custom {
--_bg: linear-gradient(hsl(228 94% 67%), hsl(228 81% 59%));
--_border: hsl(228 89% 63%);
--_text: hsl(228 89% 100%);
--_ink-shadow: 0 1px 0 hsl(228 57% 50%);
--_highlight: hsl(228 94% 67% / 20%);
}
大按钮
此按钮样式可通过修改 --_size
自定义属性来实现。内边距和其他空间元素相对于此尺寸,会根据新尺寸按比例缩放。
.btn-large {
--_size: 1.5rem;
}
图标按钮
此图标效果与我们的按钮样式没有任何关系,但它显示了如何仅用几个 CSS 属性来实现该效果,以及按钮对非内嵌 SVG 图标的处理效果。
[data-icon="cloud"] {
--icon-cloud: url("https://api.iconify.design/mdi:apple-icloud.svg") center / contain no-repeat;
-webkit-mask: var(--icon-cloud);
mask: var(--icon-cloud);
background: linear-gradient(to bottom, var(--_accent-dark), var(--_accent-light));
}
总结
现在你已经知道我是怎么做的,希望你怎么办 ‽ 🙂?
让我们来了解一下我们采用的方法多样化,并了解在 Web 上构建网站的所有方法。
只需创建一个演示,点击 tweet me 链接,我就会将其添加到下方的“社区混剪”部分中!
社区混剪作品
此处尚无可显示的内容。
资源
- GitHub 上的源代码