首发于极光日报

手把手教你用 SVG 符号和 CSS 变量做出彩色图标

简评:使用图片和 CSS 样式来制作网页图标的日子已经过去了,随着网页字体的爆发,图标字体成为展示图标的第一解决方案。

字体都是向量,所以你不必担心分辨率的问题。它们能作为文本来用 CSS 属性修饰,所以你能够完全掌控它们的大小,颜色以及样式。还可以添加变换,效果以及装饰比如旋转,下划线或阴影。

然而图标字体还不够完美,这就是为什么越来越多的人使用内嵌 SVG 图片的原因。CSS Tricks 写了一个图标字体和原生的 SVG 元素相比的诸多缺点:清晰度,位置,因为受到跨域加载,浏览器兼容,以及广告拦截器等因素影响甚至称得上失败。现在通过一个安全的选择来制作图标字体完全可以避免大多数这种问题。

然而,还有一个图标字体绝对不能做到的问题:多颜色支持。只有 SVG 能做到。

太长,不读:这篇文章将深入探讨怎么做以及为什么。如果你想理解整个过程,就继续阅读。如果想直接查看代码,可以点击 CodePen.


设置 SVG 符号图标

内嵌的 SVG 的问题在于它们的冗余。你肯定不想在每次使用同一个图标时复制粘贴所有的坐标。这显得笨拙,可读性差,难以维护。

使用 SVG 符号图标,你只要复制整个 SVG 元素一次,然后你就可以用一个引用在任何地方实例化它们。

从内嵌 SVG 开始,隐藏它,并用一个 <symbol> 包裹,然后加上 id 属性:

<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
  <symbol id="my-first-icon" viewBox="0 0 20 20">
    <title>my-first-icon</title>
    <path d="..." />
  </symbol>
</svg>

一次包含完整的 SVG 标记并隐藏在 HTML 里。

然后,只要用 <use> 元素来实例化这个图标就行了:

<svg>
  <use xlink:href="#my-first-icon" />
</svg>

这样就可以展示你之前的 SVG 图标了:

就是辣么简单!对吧?

你可能注意到了有意思的 xlink:href 属性:这是你的实例和原始的 SVG 之间的链接。

值得一提的是 xlink:href 是一个被弃用的 SVG 属性。即使大多数浏览器仍然支持,你都应该使用 href 来代替它。问题是,有些浏览器比如 Safari 还不支持通过 href 属性引用 SVG 资源,所以你还要提供 xlink:href 属性。

为了安全起见,同时提供两个属性。


添加一些颜色

不像字体,color 属性对于 SVG 图标没有任何作用:你必须使用 fill 属性来定义一个颜色。这意味着它们不像图标字体那样继承父类的文本颜色,但你仍然可以用 CSS 来为它们添加样式。

// HTML
<svg class="icon">
  <use xlink:href="#my-first-icon" />
</svg>
// CSS
.icon {
  width: 100px;
  height: 100px;
  fill: red;
}

这里,你可以用不同的填充颜色创建另一个同样的图标实例。

// HTML
<svg class="icon icon-red">
  <use xlink:href="#my-first-icon" />
</svg>
<svg class="icon icon-blue">
  <use xlink:href="#my-first-icon" />
</svg>
// CSS
.icon {
  width: 100px;
  height: 100px;
}
.icon-red {
  fill: red;
}
.icon-blue {
  fill: blue;
}

这会起作用,但不是我们确切想要的。到这里,我们刚才完成的步骤可以得到一个常规的图标字体。我们想要的是对于图标的每个部分可以有不同的颜色。我们想要用不同的颜色填充每个 path 而不用更改其他实例,必要的时候可以重写。

一开始,你可能会想到用特殊性:

// HTML
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
  <symbol id="my-first-icon" viewBox="0 0 20 20">
    <title>my-first-icon</title>
    <path class="path1" d="..." />
    <path class="path2" d="..." />
    <path class="path3" d="..." />
  </symbol>
</svg>
<svg class="icon icon-colors">
  <use xlink:href="#my-first-icon" />
</svg>
// CSS
.icon-colors .path1 {
  fill: red;
}
.icon-colors .path2 {
  fill: green;
}
.icon-colors .path3 {
  fill: blue;
}

然而这并没什么用。

我们尝试为 .path1, .path2.path3 添加样式,就好像它们被 .icon-colors 嵌套一样,但技术上来说并非如此<use> 不是一个被你定义的 SVG 的占位符。 它是一个引用,复制了指向暗处的 DOM 的内容。?

那我们该怎么做?我们怎样才能影响不在 DOM 中的子元素?


CSS 变量来帮忙

在 CSS 中,一些属性从祖先到孩子都继承了。如果你分配给 body 分配一个文本颜色,所有页面中的文本都会继承这个颜色,除非颜色被重写。祖先不知道孩子,但可继承的样式被传递下来。

我们开始的示例中,继承了 fill 属性。再看一次,你会看到我们声明的 fill 颜色被添加到实例中,而不是定义的 SVG。这就是我们能够为每个实例添加不同颜色的原因。

现在问题来了:我们想要将不同的颜色传递给原始 SVG 的不同的路径,但我们只能继承一个 fill 属性。

看看 CSS 变量。

CSS 变量可以像其他属性那样声明在规则集里。你可以为它取任意的名字,并分配任意的有效的 CSS 值。然后你可以为它声明一些值或者任意的孩子属性,它能够被继承

.parent {
  --custom-property: red;
  color: var(--custom-property);
}

所有 .parent 的孩子都会有红色的文本。

.parent {
  --custom-property: red;
}
.child {
  color: var(--custom-property);
}

所有嵌套在 .parent.child 元素也会有红色的文本。

现在将这个概念应用到我们的 SVG 符号。我们将在 SVG 定义的每个路径里用 fill 属性,然后把它们的值设置为不同的 CSS 变量。这样一来,我们就能分配不同的颜色了。

// HTML
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
  <symbol id="my-first-icon" viewBox="0 0 20 20">
    <title>my-first-icon</title>
    <path fill="var(--color-1)" d="..." />
    <path fill="var(--color-2)" d="..." />
    <path fill="var(--color-3)" d="..." />
  </symbol>
</svg>
<svg class="icon icon-colors">
  <use xlink:href="#my-first-icon" />
</svg>
// CSS
.icon-colors {
  --color-1: #c13127;
  --color-2: #ef5b49;
  --color-3: #cacaea;
}

然后……大功告成!?

从现在开始,我们只需要用不同的颜色的 CSS 类创建不同的实例。

// HTML
<svg class="icon icon-colors-alt">
  <use xlink:href="#my-first-icon" />
</svg>
// CSS
.icon-colors-alt {
  --color-1: brown;
  --color-2: yellow;
  --color-3: pink;
}

如果你只想使用单色的图标,你不用在每个 CSS 变量里重复相同的颜色。你可以使用单个 fill 来代替:因为 CSS 变量没有被定义,所以将会回滚到你的 fill 声明。

.icon-monochrome {
  fill: grey;
}

你的 fill 声明会起作用因为 fill 属性的值在原始的 SVG 上是没有定义的 CSS 变量。


我的 CSS 变量名应该叫什么?

在 CSS 中命名通常有两种方式:描述性的或者语义性的。描述性意味着什么颜色就叫什么:如果你存储 #ff0000,那就叫它 --red。语义性意味着这种颜色是怎么用的就叫什么。比如你用 #ff0000 填充咖啡杯把手,你就叫它 --cup-handle-color

描述性的名字可能是你下意识的选择。它看起来更清爽,因为 #ff0000 不仅可以用来填充咖啡杯把手,还可以为其他东西上色。当别的图标需要上红色时,一个 --red 的 CSS 变量是可以重用的。毕竟,这就是 CSS 实用至上原则的体现,它也的确是个好体制

问题在于,在我们的案例中我们不能粒度类应用到我们想要的样式。不能用实用至上原则,因为我们有每个图标的引用,我们必须通过类变量来为它们添加样式。

使用语义化的名字,比如 --cup-handle-color 在这种情况下这样才比较有意义。当你想要改变某个图标的部分颜色时,你瞬间就能知道它是什么,应该重写成什么。类名和你分配的无论什么颜色都能保持关联。


默认或不默认

将你的图标设置成多种颜色作为默认状态是很吸引人的。用这种方式,你可以直接使用它们不需要添加额外的样式,只有在需要改动时,才会添加你自己的类。

可以通过两种方式实现: :root 以及 var() default.


:root

你可以用 :root 里定义你所有的 CSS 变量。这样一来可以把所有的变量放在同一个地方,并允许你“共享“类似的颜色。 :root 有最低的优先权,所以很容易可以重写它。

:root {
  --color-1: red;
  --color-2: green;
  --color-3: blue;
  --color-4: var(--color-1);
}
.icon-colors-alt {
  --color-1: brown;
  --color-2: yellow;
  --color-3: pink;
  --color-4: orange;
}

然而,这种方法有一些主要的缺点。首先,让颜色的定义和它们各自的图标分离会变得混乱。当你想要重写它们时,你必须在 :root 和当前的类之间往返。更重要的是,它不会限制你的 CSS 变量,从而避免你使用相同的名字。

大多数时候,当一个图标仅使用一种颜色,我会用 --fill-color 。它简单易懂,当你只想用一种颜色填充所有的图标时,这种方式也比较行得通。如果我必须在 :root 中声明所有的变量,我不能有多个 --fill-color 。我必须定义 --fill-color-1, --fill-color-2等等,或者使用命名空间比如 --star-fill-color, --cup-fill-color


var() default

用来将 CSS 变量分配给某个属性的 var() 函数可以将第二个参数作为默认值。

<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
  <symbol id="my-first-icon" viewBox="0 0 20 20">
    <title>my-first-icon</title>
    <path fill="var(--color-1, red)" d="..." />
    <path fill="var(--color-2, blue)" d="..." />
    <path fill="var(--color-3, green)" d="..." />
  </symbol>
</svg>

除非你定义 --color-1,--color-2--color-3, 否则图标会使用默认的值为每个 <path> 填充颜色。这解决了之前使用 :root 时全局范围的问题,但是要注意:你现在使用了默认值,并且它正在生效。结果是,你不能够使用单个 fill 声明来定义单色的图标了。你必须为每个 CSS 变量分配颜色,一个一个地。

设置默认值很有用,但它是种权衡。我建议你不要将这种做法成为一种习惯,只有当它在特定的项目有意义时才使用。


浏览器兼容

CSS 变量对于绝大多数现代浏览器都是兼容的,但正如你所想的那样,IE 浏览器完全不支持。甚至 IE 11 也不支持,自从 Edge 的开发被搁置后,它再也没机会赶上了。

现在,仅仅因为一个浏览器不支持这个功能,这并不意味着你要推翻一切来迎合它。这种情形有更加优雅的做法:为现代浏览器提供多彩图标,为其他的老版本浏览器提供回滚的填充颜色。

你想要做的是当 CSS 变量不支持时设置一个会起作用的声明,这可以通过为 fill 属性设置回滚颜色来实现:当支持 CSS 变量时,它不会生效,如果不支持,你的 fill 声明才会生效。如果你使用 Sass,可以抽象到@mixin 里:

@mixin icon-colors($fallback: black) {
  fill: $fallback;
  @content;
} 

现在我们可以定义我们的颜色方案而不用担心浏览器兼容问题了。

.cup {
  @include icon-colors() {
    --cup-color: red;
    --smoke-color: grey;
  };
}
.cup-alt {
  @include icon-colors(green) {
    --cup-color: green;
    --smoke-color: grey;
  };
}

如果你想要了解更多 SVG 的内容,我推荐阅读 Sara Soueidan 的博客。


原文链接:Let’s make multi-colored icons with SVG symbols and CSS variables


推荐阅读:

KenChoi:开源——设计块:轻松创建简洁,现代化的网站zhuanlan.zhihu.com图标


KenChoi:节约 Web 开发时间的 67 个工具、库和资源zhuanlan.zhihu.com图标


极光日报,极光开发者旗下媒体。

每天导读三篇英文技术文章。

发布于 2018-02-06

文章被以下专栏收录