CSS Tips 系列:
在当下,Web 应用或网站上随处可以看到 SVG 的应用。SVG 作为一种灵活的图像格式,已经成为现代 Web 设计的重要组成部分。它不仅允许图像在不同尺寸和分辨率下保持清晰度,还可以实现各种动画效果和交互效果。在许多情况下,我们会使用 SVG 的 <use>
元素来重复利用 SVG 图形,从而提高代码的可维护性和性能。
然而,有时候我们可能想对 <use>
元素的内容进行样式化,以便根据不同的场景呈现不同的外观。这就是 CSS 样式化 <use>
元素内容的重要性所在。通过 CSS,我们可以轻松地为 <use>
元素的图形内容添加样式,从而实现更灵活和多样化的视觉效果。
通过本教程,你将掌握一系列技术和技巧,使你能够灵活地样式化 <use>
元素的内容,从而为你的 Web 应用或网站增添更多的美感和吸引力。让我们开始吧,探索如何通过CSS创造出独特而精美的SVG图形效果!
始于大家最熟悉的地方
Icon 图标在 Web 应用或网站上随处可见,作为一名优秀的 Web 开发者,肯定拥有为 Web 应用或网站添加图标的技术,其中最为经典的应该是雪碧图(Sprites)技术。常见的雪碧图主要有位图和矢量图两种,而且矢量图占比越来越高。
虽然在 Web 应用或网站上使用 SVG 矢量雪碧图技术已非常成熟,但还是有不少小技巧不为人知。就在不久之前,有小伙伴私下问我,使用 SVG 的 <use>
引入的 Icon 图标,应该如何才能高度自定义,如何才能为其设置颜色,最终满足自己的需求?
在回答这些问题之前,先使用一个真实的案例向大家展示,Web 开发者平时使用 SVG 矢量图碰到的经典问题。继续以 SVG 雪碧图为例,当你使用 <use>
引用一个元素(例如 Icon 图标),代码可能如下所示:
XML
<!-- 使用 symbol 定义和管理所需要的 icon 图标 -->
<svg class="icons sr-only">
<symbol id="youtube" viewBox="0 0 1024 1024">
<title>youtube</title>
<g fill="#212121">
<path d="M832 128 192 128c-105.6 0-192 86.4-192 192l0 384c0 105.6 86.4 192 192 192l640 0c105.6 0 192-86.4 192-192L1024 320C1024 214.4 937.6 128 832 128zM384 768 384 256l320 256L384 768z" fill="#212121" p-id="1710923637258-2546986_12356" />
</g>
</symbol>
<symbol id="weibo" viewBox="0 0 1024 1024"></symbol>
<!-- 其他 symbol -->
</svg>
<!-- 使用 use 将需要的 icon 引入到 Web 应用或网站中 -->
<ul class="nav">
<li>
<a href="#">
<svg>
<use href="#twitter" />
</svg>
<span>Twitter</span>
</a>
</li>
<!-- 其他 li -->
</ul>
添加一些 CSS 代码:
CSS
.nav {
a {
color: #767c8c;
transition: color .3s ease-in;
&:hover {
color: #00b3b0;
}
}
svg {
display: block;
width: 1.5em;
height: 1.5em;
fill: currentColor;
}
use {
fill: red;
}
}
你看到的效果如下:
Demo 地址:codepen.io/airen/full/...
你可能已经发现了,虽然我们在 CSS 中给 use
设置了 fill:red
,Icon 图标的颜色并没有被调整。接下来,我们来看看有哪些姿势可以为图标设置自己喜欢的颜色。
你需要知道 use 的内容在哪里
在寻找正确答案之前,你首先要知道的是:<use>
元素克隆来的内容到底放在哪里呢?
如果你打开浏览器的开发者工具,检查 <use>
元素的实例化内容(例如"Twitter"图标),你会发现在 <use>
元素下面有一个 #shadow-root
:
它是一个 Shadow DOM!
从上面的截图中可以看出,SVG 的 <use>
元素所实例化的内容被克隆到一个由 <use>
元素"托管"的文档片段中。在这种情况下,<use>
元素被称为 Shadow Host。换句话说,<use>
元素克隆的内容以一种我们熟悉的 DOM 方式存在,但是存在于 <use>
元素托管的文档片段中,就像是一个影子一样。
对于开发者而言,不能像平时那样使用 CSS 或 JavaScript 来处理 <use>
元素托管的文档片段中的 DOM 元素,这是最令人关心的问题之一。开发者希望能够覆盖存在于 Shadow DOM 中的内容,特别是对于样式设置。
然而,实际情况并不如我们想象的那么简单。我们不能以熟悉的方式处理 Shadow DOM 中元素的样式。例如,下面这样的选择器无法选中 Shadow DOM 中的 DOM 元素:
CSS
use path {
fill: red;
}
这意味着,Web 开发者无法使用常规的 CSS 选择器来访问 Shadow DOM 中的元素。也是基于这个原因,不少 Web 开发者无法使用自己所掌握的 CSS 知识来解决相应的问题。如果你是这些 Web 开发者当中的一员,那么接下来的内容你将会获益不浅!
简单粗暴的删除法
以下面这个示例为例,它是 SVG 绘制的"咖啡"图标,并且存储在 <symbol>
中:
XML
<svg xmlns="http://www.w3.org/2000/svg" class="sr-only">
<symbol id="icon-coffee" viewBox="0 0 20 20">
<title>icon-coffee</title>
<path fill="#000" d="M15,17H14V9h3a3,3,0,0,1,3,3h0A5,5,0,0,1,15,17Zm1-6v3.83A3,3,0,0,0,18,12a1,1,0,0,0-1-1Z"/>
<rect fill="#000" x="1" y="7" width="15" height="12" rx="3" ry="3"/>
<path fill="#000" d="M7.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,0,1-1.79.89Z"/>
<path fill="#000" d="M3.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z"/>
<path fill="#000" d="M11.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z"/>
</symbol>
</svg>
就上面的代码而言,如果你没有使用诸如 <use>
元素引用它的话,它是不会在 Web 上呈现的:
XML
<svg class="icon" aria-hidden="true">
<use href="#icon-coffee" />
</svg>
这个时候,你在浏览器中看到的是一个黑色的"咖啡"图标(包括咖啡杯和烟两个部分):
如果你希望得到是一个其他颜色的图标,例如 lime
颜色。我们是无法直接通过普通的 CSS 选择器来选中 <use>
元素的后代元素,但又为了需要改变图标颜色,很多 Web 开发者会考虑直接在 <symbol>
中修改元素的 fill
属性。例如:
XML
<svg xmlns="http://www.w3.org/2000/svg" class="sr-only">
<symbol id="icon-coffee" viewBox="0 0 20 20">
<title>icon-coffee</title>
<path fill="lime" d="M15,17H14V9h3a3,3,0,0,1,3,3h0A5,5,0,0,1,15,17Zm1-6v3.83A3,3,0,0,0,18,12a1,1,0,0,0-1-1Z"/>
<rect fill="lime" x="1" y="7" width="15" height="12" rx="3" ry="3"/>
<path fill="lime" d="M7.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,0,1-1.79.89Z"/>
<path fill="lime" d="M3.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z"/>
<path fill="lime" d="M11.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z"/>
</symbol>
</svg>
这种方式可以使你得到一个绿色的图标。
甚至你可能会考虑,将 <symbol>
中绘制图形元素的样式属性都从元素中删除。例如,移除上面示例代码中 <rect>
和 <path>
元素的 fill
属性,然后在 <use>
元素上应用一个 fill
属性,使其后代元素继承该属性:
XML
<svg xmlns="http://www.w3.org/2000/svg" class="sr-only">
<symbol id="icon-coffee" viewBox="0 0 20 20">
<title>icon-coffee</title>
<path d="M15,17H14V9h3a3,3,0,0,1,3,3h0A5,5,0,0,1,15,17Zm1-6v3.83A3,3,0,0,0,18,12a1,1,0,0,0-1-1Z"/>
<rect x="1" y="7" width="15" height="12" rx="3" ry="3"/>
<path d="M7.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,0,1-1.79.89Z"/>
<path d="M3.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z"/>
<path d="M11.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z"/>
</symbol>
</svg>
<svg class="icon" aria-hidden="true">
<use href="#icon-coffee" fill="lime" />
</svg>
这种方式你同样可以获得所需要的图标。但问题来了。
首先,fill
将被 <use>
的所有后代元素继承,即使你可能不想对所有后代元素进行样式化。也就是说,如果 <use>
内只有一个元素,或者你只需要一个单色图标,这种方式不会有任何问题。反之,如果 <use>
内有多个元素,并且是一个彩色图标(哪怕是两种颜色),这种方式会使你得不到最终想要的结果。
其次,你的 SVG 代码可能是通过诸如 Figma 图形设计软件导出或从设计师那里获得,或从第三方平台获得,并且出于任何原因,你可能没有权限或机会修改 SVG 代码。那么上面这两种方式也无法让你获得预期的图标。即使你可以有权限编辑 SVG 代码,我个人也强烈建议你不要这样做。因为:
-
在大多数情况下,这些属性的值都是黑色(我们这里讨论的是
fill
属性),可以称为浏览器的默认值。一旦你移除这些属性,你不得不重置它们,否则它将以黑色形式存在,除非你刚好需要 -
你可能需要重置所有值,例如
fill
、stroke
、stroke-width
等
也就是说,我们应该找到一些更合理的方式对 <use>
元素的内容进行样式化处理。
利用 CSS 的继承特性
首先,我们知道,use
元素的内容会继承来自 use
的样式。只不过我们所熟悉的选择器无法轻易让你穿透 Shadow DOM,选中 use
的后代元素。但我们可以利用 CSS 样式的继承(inherit
)特性,让外部样式声明的属性样式覆盖来自 <use>
的继续值。
继续以上面的代码为例,我们希望得到不同颜色的图标:
XML
<svg xmlns="http://www.w3.org/2000/svg" class="sr-only">
<symbol id="icon-coffee" viewBox="0 0 20 20">
<title>icon-coffee</title>
<path fill="#000" d="M15,17H14V9h3a3,3,0,0,1,3,3h0A5,5,0,0,1,15,17Zm1-6v3.83A3,3,0,0,0,18,12a1,1,0,0,0-1-1Z"/>
<rect fill="#000" x="1" y="7" width="15" height="12" rx="3" ry="3"/>
<path fill="#000" d="M7.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,0,1-1.79.89Z"/>
<path fill="#000" d="M3.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z"/>
<path fill="#000" d="M11.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z"/>
</symbol>
</svg>
XML
<svg class="icon" aria-hidden="true">
<use href="#icon-coffee" class="coffee-1" />
</svg>
<svg class="icon" aria-hidden="true">
<use href="#icon-coffee" class="coffee-2" />
</svg>
<svg class="icon" aria-hidden="true">
<use href="#icon-coffee" class="coffee-3" />
</svg>
我们使用 <use>
渲染了多个图标:
现在让我们尝试为每个图标实例更改填充颜色:
CSS
.coffee-1 {
fill: lime;
}
.coffee-2 {
fill: skyblue;
}
.coffee-3 {
fill: brown;
}
图标的填充颜色仍然不会改变,因为 <use>
元素的后代元素(<rect>
和 <path>
)上的 fill="#000"
正在覆盖继承的颜色值。
为了防止这种情况发生,我们需要强制 <rect>
和 <path>
元素继续颜色值:
CSS
:is(rect, path) {
svg & {
fill: inherit;
}
}
在我们这个示例,需要强制的是 <rect>
和 <path>
,换成别的图形,你可以还需要对其他元素做类似操作。你可以来点粗暴的方式,那就是将 svg
的所有后代元素的 fill
属性的值重置为 inherit
:
CSS
svg * {
fill: inherit;
}
现在,我们在每个 <use>
元素上设置的颜色被应用到了它的每个后代元素上。
通过种方式,你可以获得任意你想的颜色图标:
Demo 地址:codepen.io/airen/full/...
特别声明,CSS 的 inherit
是一个很有意思的特性,与其具有同等地位的还有 initial
、unset
和 revert
。如果你对它们不怎么了解,那么强烈建议你移步阅读《现代 CSS》中的 《CSS 显式默认值:inherit,initial,unset 和 revert》!
CSS 的 currentColor 是个神器
如果仅仅是调整图标颜色,那么
currentColor
将是一把神器。
上面这种方案,能够强制样式属性从 <use>
样式中继承是很强大,但是如果你有一个具有多个元素的图标,并且你不希望所有这些元素都从 <use>
继续相同的填充颜色,那么你就需要考虑其他的解决方案。继续以上面的"咖啡"图标为例,假设你希望"咖啡杯"与"咖啡热气"的填充颜色不相同,怎么办?
针对于上图这种情景,我们可以使用 CSS 的 currentColor
变量来为 <use>
元素的内容设置样式。使用 CSS 的 currentColor
变量结合上述技术,我们可以在一个元素上指定两种不同的颜色,而不仅仅是一种。这种技术的背后思想是在 <use>
上同时使用 fill
和 color
属性,然后利用 currentColor
的变量特性,使这些颜色继承到 <use>
的内容中。
利用这种技术,我们需要对 <symbol>
做一些调整。例如上面的"咖啡"图标为例,我在 <symbol>
中新增了两个 <g>
元素,它将"咖啡杯"和"烟"分成两个组,并且将 fill
属性移至 <g>
元素。
XML
<svg xmlns="http://www.w3.org/2000/svg" class="sr-only">
<symbol id="icon-coffee" viewBox="0 0 20 20">
<title>icon-coffee</title>
<g class="cup" fill="currentColor">
<path d="M15,17H14V9h3a3,3,0,0,1,3,3h0A5,5,0,0,1,15,17Zm1-6v3.83A3,3,0,0,0,18,12a1,1,0,0,0-1-1Z"/>
<rect x="1" y="7" width="15" height="12" rx="3" ry="3"/>
</g>
<g class="smoke">
<path d="M7.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,0,1-1.79.89Z"/>
<path d="M3.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z"/>
<path d="M11.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z"/>
</g>
</symbol>
</svg>
注意,我在 .cup
(咖啡杯)的 <g>
元素上设置了 fill
为 currentColor
,同时将 .smoke
和其子元素的 fill
移除。如果我们继续以 inherit
来处理,事情反而会变得更为复杂一些。现在这样做,有一个很明显的优势,fill
为 currentColor
的 SVG 元素,它将继承 <use>
元素的 color
值(咖啡杯),未设置 fill
的 SVG 元素,它将直接继承 <use>
元素的 fill
值(烟)。如果我们继续使用 inherit
关键词来强制继承 <use>
的值,那么两个 <g>
元素(.cup
和 .smoke
)都将继承相同的值,currentColor
将不再起作用。
现在,同时给 <use>
元素指定 fill
和 color
属性值,我们就可以使"咖啡杯和其上面的烟"具有不同的颜色:
CSS
.coffee-1 {
fill: red;
color: orange;
}
.coffee-2 {
fill: pink;
color: lime;
}
.coffee-3 {
fill: yellow;
color: #f26ace;
}
每个 <use>
元素都有自己的填充和颜色值。就这个示例来说,.cup
将会继承 <use>
元素的 color
属性的值,因为它的 fill
为 currentColor
(在<symbol>
中);.smoke
将会继承 <use>
元素的 fill
属性的值,因为它没有设置 fill
属性值(在<symbol>
中)。
你可以尝试在下面的示例中,随意调整 fill
和 color
的颜色值,将会获得任意你喜欢的图标(双色):
Demo 地址:codepen.io/airen/full/...
注意,currentColor
也适用于单色图标,这比使用 inherit
还要更简便,唯一的要求是,需要在 <symbol>
中将 fill
属性的值设置为 currentColor
。你可以通过 <g>
元素,避免在每个元素上重置 fill
属性的值为 currentColor
:
XML
<svg xmlns="http://www.w3.org/2000/svg" class="sr-only">
<symbol id="icon-coffee" viewBox="0 0 20 20">
<title>icon-coffee</title>
<g fill="currentColor">
<path d="M15,17H14V9h3a3,3,0,0,1,3,3h0A5,5,0,0,1,15,17Zm1-6v3.83A3,3,0,0,0,18,12a1,1,0,0,0-1-1Z" />
<rect x="1" y="7" width="15" height="12" rx="3" ry="3" />
<path d="M7.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,0,1-1.79.89Z" />
<path d="M3.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z" />
<path d="M11.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z" />
</g>
</symbol>
</svg>
<svg class="icon-coffee" aria-hidden="true">
<use href="#icon-coffee" class="coffee" />
</svg>
CSS
.coffee {
color: lime;
}
Demo 地址:codepen.io/airen/full/...
王者归来:不要忘了 NB 的 CSS 自定义属性
继续加码,如果图标不是单色,也不仅是双色,而是更多的色彩呢?
现在我们需要更多的变量。这也意味着,仅使用 currentColor
这个单一变量已经无法满足超出双色的情景了。庆幸的是,我们可以借助现代 CSS 中的自定义属性,即 CSS 变量来设置更多的变量。
使用 CSS 变量来样式化 <use>
内容与使用 currentColor
类似,首先我们需要将 <symbol>
中每个元素的 fill
属性值设置为 CSS 自定义属性:
XML
<svg xmlns="http://www.w3.org/2000/svg" class="sr-only">
<symbol id="icon-coffee" viewBox="0 0 20 20">
<title>icon-coffee</title>
<path fill="var(--fill-1, lime)" d="M15,17H14V9h3a3,3,0,0,1,3,3h0A5,5,0,0,1,15,17Zm1-6v3.83A3,3,0,0,0,18,12a1,1,0,0,0-1-1Z" />
<rect fill="var(--fill-2, lime)" x="1" y="7" width="15" height="12" rx="3" ry="3" />
<path fill="var(--fill-3, lime)" d="M7.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,0,1-1.79.89Z" />
<path fill="var(--fill-4, lime)" d="M3.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z" />
<path fill="var(--fill-5, lime)" d="M11.07,5.42a5.45,5.45,0,0,1,0-4.85,1,1,0,0,1,1.79.89,3.44,3.44,0,0,0,0,3.06,1,1,0,1,1-1.79.89Z" />
</symbol>
</svg>
<svg class="icon-coffee" aria-hidden="true">
<use href="#icon-coffee" class="coffee" />
</svg>
上面的代码中,我们为每个元素都设置了一个 CSS 自定义属性,并且提供了一个备用值 lime
。当你没有给每个变时显式指定值时,都将会使用备用值 lime
作为每个自定义属性的值。因此,你看到的咖啡杯将是一个纯色(lime
)。
基于这个前提,你可以在 <use>
中为 CSS 自定义属性指定值:
CSS
.coffee {
--fill-1: red;
--fill-2: green;
--fill-3: blue;
--fill-4: orange;
--fill-5: pink;
}
这样你就可以非常容易得到一个多色有图标:
Demo 地址:codepen.io/airen/full/...
通过利用CSS 层叠,尽管在 Shadow DOM中,对 <use>
元素的内容进行样式化可以变得不那么复杂。而且借助CSS 自定义属性(无论是仅使用 currentColor
还是自定义属性),我们可以穿透 Shadow DOM的边界,按照我们的喜好定制图形,同时为任何出现问题时提供非常好的回退机制。
就我个人而言,我对 CSS 自定义属性与 SVG 结合的功能非常兴奋。我喜欢它们在一起时的强大功能,特别是考虑到我们拥有的出色回退机制。但需要知道的是,CSS 自定义属性非常的强大,它可以帮助以更轻便的方式做更多复杂的事情,但它也有很多细节需要注意。不过,这部分内容已超出这节课的范畴,如果你感兴趣的话,可以移步阅读《CSS 自定义属性你知道多少》。
写在最后
最后以两个真实的案例来结束今天的分享。首先分享 @Jakob Eriksen 在 CodePen 提供的案例:
Demo 地址:codepen.io/jakob-e/ful...
再来一个爆炸的魔方:
Demo 地址:codepen.io/airen/full/...
特别声明:如果你对 SVG 和 CSS 更多技巧感兴趣,请关注我的第五本小册《深入浅出 SVG:你需要知道的关于 SVG 的一切》。温馨提示,提前预定可享受五折优惠,如果您有这个意向,请发邮件到 airenliao@gmail.com ,或在下面评论中留言。
如果你觉得该教程对你有所帮助,请给我点个赞。要是你喜欢 CSS ,或者想进一步了解和掌握 CSS 相关的知识,请关注我的专栏,或者移步阅读下面这些系列教程: