我最喜欢的向网站添加图标的方法之一是将它们作为 data URL 背景图像添加到伪元素(例如 ::after
)中。 这种技术有很多优点
- 除了 CSS 文件外,它们不需要任何额外的 HTTP 请求。
- 使用
background-size
属性,您可以将伪元素设置为所需的任何大小,而不必担心它们会溢出边界(或被截断)。 - 它们被屏幕阅读器忽略(至少在我的 Mac 上使用 VoiceOver 进行测试时是这样),因此对于仅用于装饰的图标来说很好。
但这种技术也有一些缺点
- 当用作
background-image
data URL 时,您将无法使用“fill”或“stroke” CSS 属性更改 SVG 的颜色(与使用文件名引用相同,例如url( 'some-icon-file.svg' )
)。 我们可以 使用filter()
作为替代方案,但这可能并不总是可行的解决方案。 - 当用作 data URL 时,SVG 标记看起来可能很大且难看,当您需要在多个位置使用图标和/或必须更改它们时,这会使它们难以维护。
我们将在本文中解决这两个缺点。
情况
让我们构建一个使用强大的图标系统的网站,假设它有几个不同的按钮图标,它们都指示不同的操作
- 用于可下载内容的“下载”图标
- 用于将我们带到另一个网站的按钮的“外部链接”图标
- 用于将我们带到流程的下一步的“右箭头”图标
一开始,我们就有三个图标。 虽然这可能看起来不多,但我已经开始担心,当我们将其扩展到更多图标(例如社交媒体网络等)时,它的可维护性会怎么样。 为了本文的目的,我们将停留在这三个图标上,但您可以想象,在一个复杂的图标系统中,这很快就会变得非常难以控制。
是时候开始编写代码了。 首先,我们将设置一个基本的按钮,然后通过使用 BEM 命名约定,我们将为其对应的按钮分配正确的图标。(此时,需要提醒您,我们将用 Sass 编写所有内容,或者更确切地说,是 SCSS。 为了便于讨论,假设我正在运行 Autoprefixer 来处理 appearance
属性之类的事情。)
.button {
appearance: none;
background: #d95a2b;
border: 0;
border-radius: 100em;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 18px;
font-weight: 700;
line-height: 1;
padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;
position: relative;
text-align: center;
text-transform: uppercase;
transition: background-color 200ms ease-in-out;
&:hover,
&:focus,
&:active {
background: #8c3c2a;
}
}
这为我们提供了一个简单、吸引人、橙色的按钮,它在悬停、聚焦和活动状态下会变成更深的橙色。 它甚至为我们提供了添加图标的空间,所以让我们现在使用伪元素添加它们
.button {
/* everything from before, plus... */
&::after {
background: center / 24px 24px no-repeat; // Shorthand for: background-position, background-size, background-repeat
border-radius: 100em;
bottom: 0;
content: '';
position: absolute;
right: 0;
top: 0;
width: 48px;
}
&--download {
&::after {
background-image: url( 'data:image/svg+xml;utf-8,' );
}
}
&--external {
&::after {
background-image: url( 'data:image/svg+xml;utf-8,' );
}
}
&--caret-right {
&::after {
background-image: url( 'data:image/svg+xml;utf-8,' );
}
}
}
让我们在这里暂停一下。 虽然我们通过声明所有按钮共有的属性尽可能地保持 SCSS 整洁,然后仅在每个类别的基础上指定背景 SVG,但它已经开始看起来有点难以控制了。 这就是我之前提到的关于 SVG 的第二个缺点:必须在 CSS 代码中使用庞大、难看的标记。
另外,请注意,我们是如何在 SVG 内部定义 fill 和 stroke 颜色的。 在某些时候,浏览器决定我们都熟悉且喜欢的井号(“#”)存在安全风险,并宣布 他们不再支持包含它们的 data URL。 这给我们留下了三种选择
- 将我们的 data URL 从标记(就像我们这里一样)转换为 base-64 编码的字符串,但这使它们比以前更难维护,因为它们完全被混淆了;或者
- 使用
rgba()
或hsla()
表示法,这并不总是直观的,因为许多开发人员多年来一直在使用十六进制;或者 - 将我们的井号转换为它们的 URL 编码等效项,“%23”。
我们将使用第三种选择,并解决这个浏览器限制。(但是,我在这里要提到,这种技术确实适用于 rgb()
、hsla()
或任何其他有效的颜色格式,甚至是 CSS 命名颜色。 但是,请不要在生产代码中使用 CSS 命名颜色。)
迁移到映射
此时,我们只有三个完全声明的按钮。 但我不喜欢像这样将它们直接转储到代码中。 如果我们需要在其他地方使用相同的图标,我们将不得不复制粘贴 SVG 标记,或者我们可以将它们分配给变量(Sass 或 CSS 自定义属性),并以这种方式重复使用它们。 但我将选择 第三扇门后面的东西,并切换到使用 Sass 最强大的功能之一:映射。
如果您不熟悉 Sass 映射,它们本质上是 Sass 版本的关联数组。 与按数字索引的项目数组不同,我们可以分配一个名称(一个键,如果您愿意),以便我们可以通过一些逻辑且易于记住的东西来检索它们。 因此,让我们构建一个包含三个图标的 Sass 映射
$icons: (
'download': '',
'external': '',
'caret-right': '',
);
这里需要注意两点:我们没有在任何这些图标中包含 data:image/svg+xml;utf-8,
字符串,只有 SVG 标记本身。 该字符串在每次需要使用这些图标时都是相同的,所以为什么要重复自己并冒犯错误的风险呢? 让我们将其定义为一个独立的字符串,并在需要时将其附加到图标标记前面
$data-svg-prefix: 'data:image/svg+xml;utf-8,';
需要注意的另一件事是,我们并没有真正让我们的 SVG 变得更漂亮;没有办法做到这一点。 我们正在做的是将所有这些难看的标记从我们每天工作的代码中提取出来,这样我们就不必经常看到所有这些视觉混乱。 heck,我们甚至可以将其放在一个单独的部分中,我们只需要在需要添加更多图标时才需要更改它。 眼不见,心不烦!
所以现在,让我们使用我们的映射。 回到我们的按钮代码,我们现在可以用从图标映射中提取它们来替换那些图标字面量
&--download {
&::after {
background-image: url( $data-svg-prefix + map-get( $icons, 'download' ) );
}
}
&--external {
&::after {
background-image: url( $data-svg-prefix + map-get( $icons, 'external' ) );
}
}
&--next {
&::after {
background-image: url( $data-svg-prefix + map-get( $icons, 'caret-right' ) );
}
}
看起来已经好多了。 我们开始以一种使代码可读且可维护的方式抽象出我们的图标。 如果那是唯一的挑战,我们就完成了。 但在这个启发本文的真实世界项目中,我们遇到了另一个难题:不同的颜色。
我们的按钮是单色的,它们在悬停状态下会变成该颜色的更深版本。 但如果我们想要“幽灵”按钮,它们在悬停时会变成纯色呢? 在这种情况下,白色图标对于出现在白色背景上的按钮将是不可见的(并且可能在非白色背景上看起来很奇怪)。 我们需要的是每个图标的两种变体:悬停状态下的白色变体,以及与按钮的边框和文本颜色匹配的非悬停状态变体。
让我们更新按钮的基础 CSS,将其从实心按钮变成在悬停时变为实心的幽灵按钮。 我们还需要调整图标的伪元素,以便我们也可以在悬停时进行切换。
.button {
appearance: none;
background: none;
border: 3px solid #d95a2b;
border-radius: 100em;
color: #d95a2b;
cursor: pointer;
display: inline-block;
font-size: 18px;
font-weight: bold;
line-height: 1;
padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;
position: relative;
text-align: center;
text-transform: uppercase;
transition: 200ms ease-in-out;
transition-property: background-color, color;
&:hover,
&:focus,
&:active {
background: #d95a2b;
color: #fff;
}
}
现在我们需要创建我们不同颜色的图标。 一种可能的解决方案是将颜色变体直接添加到我们的映射中……以某种方式。 我们可以将新的不同颜色图标作为我们一维映射中的附加项目添加,或者将我们的映射变成二维的。
一维映射
$icons: (
'download-white': '',
'download-orange': '',
);
二维映射
$icons: (
'download': (
'white': '',
'orange': '',
),
);
无论哪种方式,这都是有问题的。 只需添加一种额外的颜色,我们就会使维护工作量增加一倍。 需要使用不同的图标更改现有的下载图标吗? 我们需要手动创建每个颜色变体才能将其添加到映射中。 需要第三种颜色吗? 现在您已经将维护成本增加了两倍。 我甚至不会介绍从多维 Sass 映射中检索值的代码,因为这不能满足我们最终的目标。 相反,我们将继续前进。
输入字符串替换
除了映射之外,本文中 Sass 的实用性在于我们如何使用它使 CSS 的行为更像一门编程语言。 Sass 具有内置函数(例如 map-get()
,我们已经看到过),它允许我们编写自己的函数。
Sass 还有一堆 内置的字符串函数,但不可思议的是,字符串替换函数不是其中之一。 太糟糕了,因为它的用处是显而易见的。 但并非所有希望都破灭。
Kitty Giradel 在 2014 年的 CSS-Tricks 上为我们提供了 Sass 版本的 str-replace()
。 我们可以在这里使用它来在我们的 Sass 映射中创建我们图标的一个版本,使用一个占位符来表示我们的颜色值。 让我们将该函数添加到我们自己的代码中
@function str-replace( $string, $search, $replace: '' ) {
$index: str-index( $string, $search );
@if $index {
@return str-slice( $string, 1, $index - 1 ) + $replace + str-replace( str-slice( $string, $index + str-length( $search ) ), $search, $replace);
}
@return $string;
}
接下来,我们将更新我们的原始 Sass 图标映射(只有我们图标的白色版本的那个映射),以将白色替换为一个占位符(%%COLOR%%
),我们可以根据需要用任何调用的颜色替换它。
$icons: (
'download': '',
'external': '',
'caret-right': '',
);
但是,如果我们尝试仅使用我们新的 str-replace()
函数和 Sass 内置的 map-get()
函数来获取这些图标,我们将得到一些庞大且难看的代码。 我宁愿将这两个函数捆绑在一起,使用一个包含两个参数的函数,以使调用我们想要的颜色图标变得尽可能简单(并且因为我特别懒,我们甚至会使颜色默认设置为白色,因此如果我们想要的是该颜色的图标,我们可以省略该参数)。
因为我们正在获取一个图标,所以它是一个“获取器”函数,因此我们将它称为 get-icon()
@function get-icon( $icon, $color: #fff ) {
$icon: map-get( $icons, $icon );
$placeholder: '%%COLOR%%';
$data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );
@return str-replace( $data-uri, '#', '%23' );
}
还记得我们说过浏览器不会渲染包含井号(octothorpe)的数据 URL 吗?是的,我们也对它进行了 str-replace()
操作,这样我们就不必再记得在颜色十六进制代码中传递 “%23” 了。
旁注:我还有一个 用于抽象颜色的 Sass 函数,但由于这超出了本文的范围,我将把您引荐到我的 get-color()
gist,供您闲暇时细读。
结果
现在我们有了 get-icon()
函数,让我们用它来实现。回到我们的按钮代码,我们可以用新的图标获取器替换 map-get()
函数。
&--download {
&::before {
background-image: get-icon( 'download', #d95a2b );
}
&::after {
background-image: get-icon( 'download', #fff ); // The ", #fff" isn't strictly necessary, because white is already our default
}
}
&--external {
&::before {
background-image: get-icon( 'external', #d95a2b );
}
&::after {
background-image: get-icon( 'external' );
}
}
&--next {
&::before {
background-image: get-icon( 'arrow-right', #d95a2b );
}
&::after {
background-image: get-icon( 'arrow-right' );
}
}
是不是容易多了?现在我们可以调用我们定义的任何图标,并使用任何需要的颜色。所有这些都通过简单、干净、逻辑的代码实现。
- 我们只需要在一个地方声明 SVG。
- 我们有一个函数可以获取任何给定颜色的图标。
- 所有内容都抽象到一个逻辑函数中,该函数的功能完全符合它的字面意思:获取 X 图标,颜色为 Y。
使其防错
我们缺少的一件事是错误检查。我非常相信静默失败……或者至少以一种对用户不可见的方式失败,但可以清楚地告诉开发人员发生了什么错误以及如何修复它。(为此,我应该比现在更频繁地使用单元测试,但这留待以后再谈。)
我们已经通过设置默认颜色(在本例中为白色)来降低函数出错的可能性。因此,如果使用 get-icon()
的开发人员忘记添加颜色,不用担心;图标将是白色的,如果这不是开发人员想要的结果,则很明显,也很容易修复。
但是,如果第二个参数不是颜色呢?假设开发人员错误地输入了颜色,以至于 Sass 处理器不再将其识别为颜色?
幸运的是,我们可以检查 $color
变量的值类型。
@function get-icon( $icon, $color: #fff ) {
@if 'color' != type-of( $color ) {
@warn 'The requested color - "' + $color + '" - was not recognized as a Sass color value.';
@return null;
}
$icon: map-get( $icons, $icon );
$placeholder: '%%COLOR%%';
$data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );
@return str-replace( $data-uri, '#', '%23' );
}
现在,如果我们尝试输入一个无意义的颜色值
&--download {
&::before {
background-image: get-icon( 'download', ce-nest-pas-un-couleur );
}
}
……我们会得到解释错误的输出
Line 25 CSS: The requested color - "ce-nest-pas-un-couleur" - was not recognized as a Sass color value.
……并且处理停止。
但是,如果开发人员没有声明图标呢?或者更可能的情况是,开发人员声明了一个 Sass 地图中不存在的图标?在这种情况下,提供一个默认图标没有意义,这就是图标一开始是必填参数的原因。但为了确保我们调用的是一个图标,并且它有效,我们将添加另一个检查
@function get-icon( $icon, $color: #fff ) {
@if 'color' != type-of( $color ) {
@warn 'The requested color - "' + $color + '" - was not recognized as a Sass color value.';
@return null;
}
@if map-has-key( $icons, $icon ) {
$icon: map-get( $icons, $icon );
$placeholder: '%%COLOR%%';
$data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );
@return str-replace( $data-uri, '#', '%23' );
}
@warn 'The requested icon - "' + $icon + '" - is not defined in the $icons map.';
@return null;
}
我们将函数的核心部分包含在一个 @if
语句中,该语句检查地图是否包含提供的键。如果是(这是我们希望看到的情况),将返回处理后的数据 URL。函数在 @return
处立即停止,这就是我们不需要 @else
语句的原因。
但如果找不到我们的图标,则将返回 null
,并在控制台输出中发出 @warn
警告,以标识问题请求,以及部分文件名和行号。现在我们确切地知道哪里错了,以及何时以及需要修复什么。
因此,如果我们不小心输入了
&--download {
&::before {
background-image: get-icon( 'ce-nest-pas-une-icône', #d95a2b );
}
}
……我们将在控制台中看到输出,Sass 处理过程在那里监视和运行
Line 32 CSS: The requested icon - "ce-nest-pas-une-icône" - is not defined in the $icons map.
至于按钮本身,图标所在区域将为空白。这不如显示我们想要的图标好,但比显示损坏的图像或其他东西好得多。
结论
在经历了这一切之后,让我们看看我们最终的处理后的 CSS
.button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: none;
border: 3px solid #d95a2b;
border-radius: 100em;
color: #d95a2b;
cursor: pointer;
display: inline-block;
font-size: 18px;
font-weight: 700;
line-height: 1;
padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;
position: relative;
text-align: center;
text-transform: uppercase;
transition: 200ms ease-in-out;
transition-property: background-color, color;
}
.button:hover, .button:active, .button:focus {
background: #d95a2b;
color: #fff;
}
.button::before, .button::after {
background: center / 24px 24px no-repeat;
border-radius: 100em;
bottom: 0;
content: '';
position: absolute;
right: 0;
top: 0;
width: 48px;
}
.button::after {
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.button:hover::after, .button:focus::after, .button:active::after {
opacity: 1;
}
.button--download::before {
background-image: url('data:image/svg+xml;utf-8,');
}
.button--download::after {
background-image: url('data:image/svg+xml;utf-8,');
}
.button--external::before {
background-image: url('data:image/svg+xml;utf-8,');
}
.button--external::after {
background-image: url('data:image/svg+xml;utf-8,');
}
.button--next::before {
background-image: url('data:image/svg+xml;utf-8,');
}
.button--next::after {
background-image: url('data:image/svg+xml;utf-8,');
}
哎呀,看起来仍然很丑,但这种丑陋是浏览器的负担,而不是我们的负担。
我已经将所有这些内容整合到 CodePen,供您分叉和实验。这个小型项目的最终目标是创建一个 PostCSS 插件来完成所有这些操作。这将提高这种技术的可用性,使每个人都能使用它,无论他们是否使用 CSS 预处理器,或者使用的是哪种预处理器。
“如果我看得更远,那是因为我站在巨人的肩膀上。”
—— 艾萨克·牛顿,1675 年
当然,我们不能不感谢那些激发这种技术的人的贡献,他们启发了我们谈论 Sass、字符串替换(尤其是)SVG。
- Str-replace 函数 由 Kitty Giraudel 创作
- 一个非常不错的 SVG 图标系统 由 Chris Coyier 创作
- SVG 与 CSS 的实际应用 由 Craig Buckler 创作
令人印象深刻,谢谢!
这真的很酷!我喜欢您如何在无需单独文件的情况下处理 SVG 背景图像中填充/描边颜色的更改。
我过去使用过 postcss-inline-svg 来实现这一点,效果很好。
仅供记录,如果您关心 IE11,您仍然需要对标签的 lt 和 gt 符号进行 URL 编码,并且不要使用任何编码。请参阅 https://css-tricks.cn/probably-dont-base64-svg/
非常好的观点,谢谢!
我喜欢您构建的静默选项。这与我过去做过的解决方案非常相似,但我专注于填充,而不是更改 SVG 字符串。干得好。
您不必使用
%%COLOR%%
,可以使用 CSS 变量,使 SVG 标记保持有效fill="var(--svg-default-fill)"
stroke="var(--svg-default-stroke)"
这些变量是唯一的,因此正则表达式匹配是可靠的。您还可以将 SVG 标记重新用在 HTML 中。
“[quote]但在生产代码中请不要使用 CSS 命名颜色。[/quote]
为什么?
几年前,我写过类似的东西,使用的是图标字体,您的代码是对使用 svg 的这种方法的重大改进!感谢分享;)
https://gist.github.com/mistergraphx/dfc1c57bd7d202bf1464
对于最近的一个项目,我不需要担心 IE11 的支持,我使用了与您相同的内联 SVG,但它是在
mask-image
声明中。这样,您可以使用 CSS 将颜色从静态更改为悬停,正如您所期望的那样。currentColor
规则与静态状态下的文本颜色匹配。示例如果您添加前缀,支持度非常好:https://caniuse.cn/#search=mask-image
有没有办法通过对特殊字符进行编码,将 IE11 支持添加到此 sass 函数中?以这个 gist 为例,但这有点让人不知所措。 https://gist.github.com/JacobDB/0ffffaf8e772c12acf7102edb8a302be