简要介绍了如何构建可自适应颜色、自适应且无障碍的 <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);
按钮文字颜色
按钮文字颜色不是白色或黑色,而是 --_accent 的深色或浅色版本,使用 hsl() 并保持色调 210 不变:
--_text-light: hsl(210 10% 30%);
--_text-dark: hsl(210 5% 95%);
--_text: var(--_text-light);
按钮背景色
按钮背景遵循相同的 hsl() 模式,但浅色主题按钮除外,这些按钮设置为白色,因此其表面看起来更贴近用户,或位于其他表面之前:
--_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);
按钮内边距
按钮中文字周围的间距使用 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);
按钮阴影
为了使阴影能够正确适应浅色模式和深色模式,它们需要同时改变颜色和不透明度。浅色主题的阴影最好是细微的,并且着色倾向于其叠加的 Surface 颜色。深色主题的阴影需要更暗、更饱和,以便叠加在较深的界面颜色上。
--_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 box-shadow 营造出这种效果:
--_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;
}
图标
对于图标处理,选择器添加了 :where() 选择器,用于直接 SVG 子元素或具有自定义属性 data-icon 的元素。使用内嵌和块逻辑属性通过自定义属性设置图标大小。描边颜色已设置,并且 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 设置为 max-inline-size,因此不会超出所需大小,而 inline-size: 100% 将允许自身缩小并适应比自身小的容器。背景颜色设置为比其他表面更深的自适应颜色,因此看起来位于文件选择器按钮后面。
: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 上构建的所有方式。
制作演示视频,通过 Twitter 向我发送链接,我会将其添加到下方的社区混音部分!
社区混音作品
此处尚无可显示的内容。
资源
- GitHub 上的源代码