用CSS给SVG <use>的内容添加样式

发布于 彦子

一篇深入探究如何给SVG<use>元素的内容添加样式的文章,并针对碰到的问题逐一解决。

svg use

SVG图形的一个最常见用例是图标系统,其中最常用的SVG sprite技术就是使用SVG<use> 元素在文档中任意位置“实例化”图标

使用<use>元素实例化图标或任何其它的SVG元素或图像,给元素添加样式时经常会碰到一些问题。这篇文章的目的是尽可能给你介绍一些方法来解决:使用<use>引入的内容添加样式受限的问题。

但是在开始之前,我们先快速浏览一下SVG的主要结构和分组元素,然后慢慢进入use的世界中,以及shadow DOM,然后重回CSS的怀抱。我们会逐步讲解为什么给<use>内容添加样式会比较麻烦,以及有什么好的解决方案。

SVG结构化、分组,以及在SVG中引用(重用)元素速览

SVG中有四个主要的元素用于在文档中定义、结构化和引用SVG代码。这些元素使得重用SVG元素变得容易,同时保持代码的简洁性和可读性。因为SVG的特性,这些元素和图形编辑器中的某些命令具有相同的功能。

这四个用于SVG分组和引用的主要元素是:<g>, <defs>, <use><symbol>

<g>元素(“group”的简写),用于给逻辑上相联系的图形元素分组。从图形编辑器的角度,如Adobe Illustrator,<g>元素提供了类似于分组对象的功能。你也可以把它想象成图形编辑器中图层的概念,因为一层也是一组元素。

当你想要应用某个样式,并希望这个样式能被组中的所有元素继承,分组元素<g>非常好用,特别是当你想要给某组元素应用动画,同时还需要保持它们彼此的空间关系的时候。

<defs>元素用来定义你之后要重用的元素。当你想要创建某一类在文档中要多次使用的“模板”时,使用<defs>定义元素。在<defs>元件中定义的元素不会在画布中渲染出来,除非你在文档的某个位置调用了它们。

<defs>可以用于定义很多东西,但是最主要的使用情景之一是定义类似渐变的图案,例如,使用这些渐变作为其它SVG元素的描边填充。它可以用来定义你想要在画布上渲染的任何元素。

<symbol>元素结合了<defs><g>元素的优点,将定义模板的元素组合在一起,以便之后在文档中的其他位置引用。和<defs>不同,<symbol>通常不用于定义图案,但是经常用于定义例如图标这样的标志,在整个文档中都可以被引用。

<symbol>元素相比其它两个元素有一个非常重要的优点:它接受一个viewBox属性,可以让它在任何视窗中自适应大小缩放渲染。

<use>元素用于引用文档中其它位置定义的元素。你可以重用已有的元素,类似于图形编辑器中的复制粘贴功能。它可以重用单个元素,也可以重用一组用<g><defs><symbol>定义的元素。

要使用一个元素,你需要通过一个标识对该元素进行引用——一个ID,即use中的xlink:href属性,以及用来给该元素定位的xy属性。你可以给use元素应用样式,这些样式也会级联应用到use元素的内容中去。

<use>的内容是什么呢?它被克隆到哪里了?CSS级联如何处理这些内容?

在我们回答这些问题之前,因为我们目前只讲了SVG结构化和分组元素,这里还有几篇值得我们继续深入学习的文章,关于viewBox属性和<symbol>的使用:

SVG <use>及shadow DOM

当你使用<use>引用元素时,代码如下:

<symbol id="my-icon" viewBox="0 0 30 30">
    <!-- icon content / shapes here -->
</symbol>

<use xlink:href="#my-icon" x="100" y="300" />

渲染在屏幕上的东西是内容定义在<symbol>内的图标,但是这不是真正渲染出的内容,而是<use>的内容,也就是<symbol>内容的一个副本或者克隆。

但是<use>元素只是一个元素,它是自闭合的。在use标签的开闭区间内没有任何内容,所以<symbol>的内容是克隆到哪里了呢?

答案是:Shadow DOM。(不知道为什么,shadow DOM总是让我想起蝙蝠侠_(:зゝ∠)_。)

什么是shadow DOM?

shadow DOM和常规的DOM很类似,不同之处在于shadow DOM不是主文档子树的一员,shadow DOM中的结点属于文档片段,基本上等同于另一棵结点树,不能像普通结点那样添加脚本和样式。这给了作者们一种方法来封装和包裹样式及脚本,当创建模块化组件时。如果你使用过HTML5的video元素,或range input类型,也很好奇video控件或者范围输入组件是从哪里来的,那么你就已经接触过shadow DOM了。

在SVG<use>元素中,引用元素的内容被复制到一个文档片段中保存,这个文档片段是由<use>保留着。<use>在这里就是一个shadow Host。

所以,<use>的内容(克隆或复制那个它引用的元素的)都表示在一个shadow文档片段中。

也就是说,它们就在那里,但是并不可见。就像普通的DOM内容一样,但是并不是在“高等级”的DOM中,并不能在主文档中被CSS选择器和JavaScript选中,它们被复制到由<use>保留的文档片段中。

现在,如果你是一个设计师,你可能会想:“ok,我了解了这东西了,但是有什么方法可以检查子文档,来看看它的真正的内容呢?”答案是:有的!你可以使用Chrome的开发者工具预览shadow DOM的内容。(现在还无法在Firefox中查看shadow DOM的内容。)但是为了完成这个,你需要先在“General”面板中勾选shadow DOM检查的选项。也就是:打开 Chrome 的开发者工具,点击右上角的“Settings”按钮勾选“Show user agent shadow DOM”。

在开发者工具中勾选了shadow DOM检查这一项之后,你可以在Elements面板中看到克隆的元素,和普通的DOM元素一样。下面的图片展示了<use>元素引用<symbol>元素的内容的示例。注意到有一个“#shadow-root”,而且当点开此片段的内容时——会发现它就是<symbol>内容的副本。

shadow DOM

检查一下这些代码,你可以看到shadow DOM和普通的DOM非常相似,除了在主文档中用CSS和JavaScript处理时有不同的特性之外。它们之间还存在其它差异,但是这一节不可能完全在讲shadow DOM,因为这真的是一个很大的概念,所以如果你想要阅读和了解更多关于它的内容的话,我推荐下面这几篇文章:

图灵社区的同学翻译的shadow DOM系列文章

对我来说,考虑如何限制和shadow DOM的交互时,我把它当成普通DOM一样,除了在用CSS(和JavaScript)添加样式时需要不同地处理。但是对于SVG开发者来说就是一个问题:shadow DOM中<use>的内容如何存在,当需要给内容应用样式或者改变样式的时候,因为我们希望可以为它们添加样式。使用<use>的目的是为了可以创建某个元素的多个不同的“副本”,很多情境下,我们想要的是可以给差异化地给不同的副本添加样式。例如,考虑一个有两种样式的logo(反转颜色的主题)或多种颜色的icon,每一个都有自己的主题。这时,我们自然而然就会想到使用CSS来完成。

也就是说,我们前面提到的shadow DOM的内容在CSS看来不能像普通DOM一样添加样式。所以,我们要怎么给它的内容添加样式呢?我们不能像这样指向<use>的路径级联:

use path#line {
    stroke: #009966;
}

因为我们不能使用普通的CSS选择器来获取shadow DOM。

有一组特殊的选择器可以让我们打破普通DOM的界限,给它里面的结点应用样式,但是这些选择器并没有很好的浏览器支持,而且相比CSS中提供的一长串用来选中普通DOM元素的选择器,它们是受限的。

此外,我们希望有一个更简单的方式来给SVG<use>的内容添加样式,而不需要去接触shadow DOM的具体内容——只使用简单的CSS和SVG。

为了实现以及获得更多一点控制,给<use>的内容添加样式,我们需要从不同的角度思考,借用CSS级联和继承的优势。

级联样式

因为SVG元素可以使用CSS通过三种不同的方法之一进行添加样式:外部的CSS样式(在外部的CSS文件中),内部样式块(<style>元素包裹),以及内联样式(在元素的style属性中)。重点在于这些级联管理是如何将样式应用到元素之上的。

除了CSS属性,SVG元素还可以使用描述属性添加样式。描述属性是在元素上设置CSS属性的简写方式。可以认为它们是特殊的样式属性。它们的目的就是给样式级联做贡献,但是可能正走在一个我们不太期望的方向上。

在下面的代码片段中,简单地展示了一个粉色的带黄色描边的圆。strokestroke-widthfill都是描述属性。

<svg viewBox="0 0 100 100">
    <circle fill="deepPink" stroke="yellow" stroke-width="5" cx="50" cy="50" r="10"></circle>
</svg>

在SVG中,所有CSS属性的子集可以通过SVG属性设置,反之亦然。这意味着,不是所有的CSS属性都可以被指定给SVG元素作为描述属性,也不是所有SVG支持的描述属性都可以在CSS中指定,虽然有很多都可以。

SVG规范列出了可以设置为CSS属性的SVG属性。其中一些属性可以和CSS共享(也就是已经可以作为CSS属性),如opacitytransform,有一些还不行,如fillstrokestroke-width

在SVG 2中,这个列表将包括xywidthheightcxcy,以及一些其它的描述属性,目前还不能在SVG 1.1中通过CSS来设置的。新的属性列表可以在SVG 2规范中找到。

如果你和我一样,那么你一定会期待描述属性可以有相比其它样式声明更高的特殊性。我的意思是,毕竟,外部的样式可以被内部的样式块覆盖,内部的样式块又可以被style属性设置的内联样式覆盖。那么这看起来是不是越“内层”的样式,优先级就越高。所以如果一个属性有了自己的特性,它是不是就更强大,因此它也就可以覆盖所有其它的样式声明。尽管这对我来说是非常棒的,但是它真正的工作原理却不是这样的。

事实上,描述属性算是比较低层级的“作者样式层叠表”,可以被其它所有的样式定义覆盖:外部的样式表,内部的样式块以及内联样式。描述属性唯一超过的就是继承样式。就是说,描述属性只可以覆盖文档中的继承样式,但是会被其它所有的样式声明覆盖_(:зゝ∠)_。

好滴~既然我们现在弄清楚了,我们回到<use>元素以及它的内容上吧。

我们现在知道我们不同使用CSS选择器给<use>中的元素设置样式。

我们知道,正如<g>元素,你应用给<use>的样式将会被它所有的后代内容继承(也就是shadow DOM中的内容)。

所以第一个改变<use>内元素的fill颜色的尝试就是给<use>元素本身应用此填充颜色,并让其继承和级联。

但是,这带来了两个问题:

  • 该填充颜色将被<use>的所有后代内容继承,甚至包括那些你并不想给它们应用样式的内容(如果你的<use>中还没有任何元素,那么这就不成问题。)
  • 如果是通过图形编辑器导出,或者是从其它设计师手中拿到的SVG,简单来说,就是你不能接触到SVG源码,那么你可能就没办法给SVG元素应用描述属性了(除非你明确指出你不希望在输出SVG的时候发生这个事情,但这是另一个话题了),这些属性的值将覆盖你给<use>应用的所有样式。现在,我假设如果你给<use>指定了样式,而且希望这些样式可以被它的后代继承,那么描述属性可能会给你带来不便。

即使你可以获取SVG的代码,你也可以摆脱描述属性,我强烈建议不要这样做,因为:

  • 删除那些用于设置某些属性的特性_(:зゝ∠)_,将会导致这些属性被重置为初始的浏览器默认值——也就是,一般情况下,所有都是黑色填充和描边(比如我们现在讨论的是颜色)。
  • 通过重置所有值,你可以强迫自己去给所有属性集指定样式,所以除非你想这么做,否则你不要想摆脱这些描述属性了。
  • 描述属性设计的初衷是作为一项降级机制,用于当你的外部样式因为某些原因不能应用的时候。如果CSS因为某些东西给搞砸而不能加载的时候,你的图标至少可以有些默认的相对漂亮的样式可以降级。这点我强烈建议保留它们。

好了,现在我们有这些属性了,但是我们还想针对不同的实例应用不同的样式,比如说,不同的图标。

需要做的就是确保我们强制描述属性继承了设置于<use>之上的样式,或者找到一个方法来让它们覆盖这些值。为了做到这一点,我们需要利用CSS的优势。

我们从最简单的实例开始,然后慢慢进入到更复杂的情景。

CSS描述属性值的介绍

描述属性可以被其它任何的样式声明覆盖。我们可以利用这个优势,用一个外部的样式声明,强制描述属性覆盖从use继承的值。

通过使用CSSinherit关键字,这会变得非常简单。看看下面的例子,我们绘制了一个冰淇淋的图标,只用一条路径完成,而且可以根据不同的情况改变填充颜色。这个图标是Erin Agnoli在Noun项目中创建的。

<svg>
  <symbol id="ic">
    <path fill="#000" d="M81,40.933c0-4.25-3-7.811-6.996-8.673c-0.922-5.312-3.588-10.178-7.623-13.844  c-2.459-2.239-5.326-3.913-8.408-4.981c-0.797-3.676-4.066-6.437-7.979-6.437c-3.908,0-7.184,2.764-7.979,6.442  c-3.078,1.065-5.939,2.741-8.396,4.977c-4.035,3.666-6.701,8.531-7.623,13.844C22.002,33.123,19,36.682,19,40.933  c0,2.617,1.145,4.965,2.957,6.589c0.047,0.195,0.119,0.389,0.225,0.568l26.004,43.873c0.383,0.646,1.072,1.04,1.824,1.04  c0.748,0,1.439-0.395,1.824-1.04L77.82,48.089c0.105-0.179,0.178-0.373,0.225-0.568C79.855,45.897,81,43.549,81,40.933z   M49.994,11.235c2.164,0,3.928,1.762,3.928,3.93c0,2.165-1.764,3.929-3.928,3.929s-3.928-1.764-3.928-3.929  C46.066,12.997,47.83,11.235,49.994,11.235z M27.842,36.301c0.014,0,0.027,0,0.031,0c1.086,0,1.998-0.817,2.115-1.907  c0.762-7.592,5.641-13.791,12.303-16.535c1.119,3.184,4.146,5.475,7.703,5.475c3.561,0,6.588-2.293,7.707-5.48  c6.664,2.742,11.547,8.944,12.312,16.54c0.115,1.092,1.037,1.929,2.143,1.907c2.541,0.013,4.604,2.087,4.604,4.631  c0,1.684-0.914,3.148-2.266,3.958H25.508c-1.354-0.809-2.268-2.273-2.268-3.958C23.24,38.389,25.303,36.316,27.842,36.301z   M50.01,86.723L27.73,49.13h44.541L50.01,86.723z"/>
  </symbol>
</svg>

这个冰淇淋图标的内容(也就是path)是定义在一个<symbol>元素中的,也就是说它们不会直接在SVG画布中渲染。

然后,我们使用<use>渲染出多个图标实例。

<svg class="icon" viewBox="0 0 100 125"> 
    <use class="ic-1" xlink:href="#ic" x="0" y="0" />
</svg>
<svg class="icon" viewBox="0 0 100 125"> 
    <use class="ic-2" xlink:href="#ic" x="0" y="0" />
</svg>

我们在CSS中设置图标的宽度和高度。我使用了viewBox一样的尺寸,但它们也不是一定要相同的。但是,为了避免SVG内多余的空白太多,保持它们的宽高比。

.icon {
    width: 100px;
    height: 125px;
}

使用上面的代码,你可以得到下面的结果:

注意我给SVG添加了一个黑色的边框,这样大家才可以看到每个图的边界,我们定义的第一个SVG图标的内容并没有渲染。这里可以提出一点:你在symbol中定义的SVG文档也会在页面中渲染出来,即使它没有包括渲染图形。为了避免这一点,确保你在第一个SVG中设置了display: none。如果你没有隐藏包含图标定义的SVG,即使你没有明确设置任何尺寸,它也会被渲染出来——浏览器默认尺寸是300x150px,这是在CSS中没有替代元素时的默认尺寸,所以你会在页面上得到一块白色的区域,尽管你并不想要这块东西。

现在让我们试试改变每个图标实例的填充颜色:

use.ic-1 {
    fill: skyblue;
}
use.ic-2 {
    fill: #FDC646;
}

即使这样写了,图标的填充颜色还是不会有任何改变,因为继承的颜色值被path元素的fill="#000"覆盖了。为了阻止这个东西,我们强制让path继承颜色值:

svg path {
    fill: inherit;
}

瞧!我们给<use>元素设置的颜色现在可以逐个应用于path了。查看下面的demo,可以照自己喜欢的去改变颜色值,创建更多实例:

现在这种技术已经非常好用,当你想要强制<use>的内容继承你设置的样式时。但是在大多数情况下,这可能不是你想要的。还有很多其它添加样式的场景,所以我们接下来会介绍一些其它的方法。

使用CSS的all属性给<use>的内容添加样式

前段时间我使用一个引用自use的图标,我想让它里面的其中一个元素可以继承所有我给<use>设置的样式,像fillstrokestroke-widthopacity甚至transform。基本上,我希望可以控制所有这些CSS属性,同时保留标签中的描述属性作为降级。

如果你发现你也处在这样一个场景中,你可能会发现这用CSS做起来非常耗时间:

path#myPath {
    fill: inherit;
    stroke: inherit;
    stroke-width: inherit;
    transform: inherit;
    /* ... */
}

看看上面的代码片段,你可以看到都是同一个模式,我们应该可以把所有这些属性结合起来,放到一个属性中,并把所有这些属性的值设置为inherit

幸运的是,这就是CSS的all属性发光发热的时候了。我之前写过关于使用all属性来给SVG的<use>内容添加样式的参考条目,但是因为我们现在的上下文环境,我们需要再看看。

使用all属性,我们可以这样写:

path#myPath {
    all: inherit;
}

这在所有支持all属性的浏览器中都工作得非常好(详细信息请查看属性参考条目),然而还有几个重点要记住:这条声明会真正地给元素的所有属性都设置从父元素继承值,包括那些你可能不想要的属性。所以除非你想要在CSS中给元素的所有属性都设置样式,否则你就不要使用它——这是一种极端的措施,当你想要暴露你的元素,然后在CSS中对它的样式属性进行完全的控制的时候才使用,这种情况比较少见。如果你使用这条声明,不在CSS中指定所有属性的值,它们就会直接往上然后级联,知道它们找到可以继承的值,大多数情况下就是浏览器的默认样式,从默认用户代理样式表加载而来。

注意这只会影响到那些可以在CSS中设置的属性,不包括那些SVG独有的属性。所以如果一个属性可以作为CSS属性设置,它就会被设置为inherit,否则就不会。

能够强制描述属性去从<use>继承样式是强大的,但是如果你的图标是由多个元素组成的呢,你肯定不想要让所有的这些元素都从use继承同一个fill颜色吧?那如果你想要给不同的use级联应用多个填充颜色怎么办呢?给use设置一个样式已经不足够了,我们需要一些其它的东西来帮助我们从正确的元素级联正确的颜色。

使用CSS的currentColor变量来给<use>内容添加样式

使用CSS的currentColor变量,并结合上面的技术,我们可以给一个元素指定两种不同的颜色,而不仅是一种。Fabrice Weinberg在他的CodePen blog写了一些关于这种技术的文章

有关于currentColor更多信息,可以参阅早前的一篇译文《使用CSS的currentColor变量扩展颜色级联》。

这种技术的内幕其实是在<use>上同时使用fillcolor属性,然后利用currentColor的变量特性,让这些颜色级联到<use>的内容上。我们先看一个代码实例,看看这是怎么搞的先。

假设我们要给这个小小的Codrops的logo添加两种颜色的样式——一个用于前面的水滴,一个用于后面的——logo的每一个实例都是采用两种颜色。

currentColor

首先,我们从上面的代码截图开始:用symbol包裹我们的图标定义,然后使用三个<use>创建三个logo实例。

<svg style="display: none;">
<symbol id="codrops" viewBox="0 0 23 30">
    <path class="back" fill="#aaa" d="M22.63,18.261c-0.398-3.044-2.608-6.61-4.072-9.359c-1.74-3.271-3.492-5.994-5.089-8.62l0,0   c-1.599,2.623-3.75,6.117-5.487,9.385c0.391,0.718,0.495,1.011,0.889,1.816c0.143,0.294,0.535,1.111,0.696,1.43   c0.062-0.114,0.582-1.052,0.643-1.162c0.278-0.506,0.54-0.981,0.791-1.451c0.823-1.547,1.649-2.971,2.469-4.33   c0.817,1.359,1.646,2.783,2.468,4.33c0.249,0.47,0.513,0.946,0.791,1.453c1.203,2.187,2.698,4.906,2.96,6.895   c0.292,2.237-0.259,4.312-1.556,5.839c-1.171,1.376-2.824,2.179-4.663,2.263c-1.841-0.084-3.493-0.887-4.665-2.263   c-0.16-0.192-0.311-0.391-0.448-0.599c-0.543,0.221-1.127,0.346-1.735,0.365c-0.56-0.019-1.095-0.127-1.599-0.313   c1.448,3.406,4.667,5.66,8.447,5.78C19.086,29.537,23.469,24.645,22.63,18.261z"/>
    <path class="front" fill="#ddd" d="M6.177,11.659c0.212,0.367,0.424,0.747,0.635,1.136c0.164,0.303,0.333,0.606,0.512,0.927   c0.683,1.225,1.618,2.898,1.755,3.937c0.144,1.073-0.111,2.056-0.716,2.769c-0.543,0.641-1.315,1.014-2.186,1.067   c-0.87-0.054-1.643-0.43-2.186-1.067c-0.604-0.713-0.858-1.695-0.715-2.771c0.137-1.036,1.072-2.712,1.755-3.936   c0.18-0.32,0.349-0.623,0.513-0.927C5.752,12.404,5.964,12.026,6.177,11.659 M6.177,5.966L6.177,5.966   c-1.02,1.649-2.138,3.363-3.247,5.419c-0.932,1.728-2.344,3.967-2.598,5.88c-0.535,4.014,2.261,7.09,5.846,7.203   c3.583-0.113,6.379-3.189,5.845-7.203c-0.255-1.912-1.666-4.152-2.598-5.88C8.314,9.329,7.196,7.617,6.177,5.966L6.177,5.966z"/>
</symbol>
</svg>
<svg height="90px" width="69px">
  <use xlink:href="#codrops" class="codrops-1"/>
</svg>
<svg height="90px" width="69px">
  <use xlink:href="#codrops" class="codrops-2"/>
</svg>
<svg height="90px" width="69px">
  <use xlink:href="#codrops" class="codrops-3"/>
</svg>

如果我们想要给<use>元素的每个实例设置fill颜色,该颜色将会两条路径都继承,最后它们就会有相同的颜色——这不是我们想要的。

所以我们不仅要指定fill颜色,还要让它按照默认方法级联,然后使用currentColor变量来确保icon前面的小水滴获取和背景不同的颜色值:该值通过color属性指定。

首先,我们需要在我们想要应用该颜色值的地方插入currentColor;进入定义图标内容的标签,在<symbol>中。代码如下:

<svg style="display: none;">
    <symbol id="codrops" viewBox="0 0 23 30">
        <path class="back" fill="#aaa" d="..."/>
        <path class="front" fill="currentColor" d="..."/>
    </symbol>
</svg>

下一步我们需要从另一个水滴中删除fill描述属性,并让它从use继承fill颜色,而不是使用inherit技术。

如果我们要使用inherit关键字来强制描述属性从use继承值,两条路径都会继承相同的值,这样currentColor就不会再产生任何效果。所以,在这种技术中,我们需要删除我们想要在CSS中设置的属性,只保留那个我们想要使用currentColor设置的值。

<svg style="display: none;">
    <symbol id="codrops" viewBox="0 0 23 30">
        <path class="back" d="..."/>
        <path class="front" fill="currentColor" d="..."/>
    </symbol>
</svg>

现在,在<use>上使用fillcolor属性,给水滴添加样式:

.codrops-1 {
  fill: #4BC0A5;
  color: #A4DFD1;
}
.codrops-2 {
  fill: #0099CC;
  color: #7FCBE5;
}
.codrops-3 {
  fill: #5F5EC0;
  color: #AEAFDD;
}

每个<use>元素有自己的fillcolor值。对单个水滴来说,fill颜色级联并被没有fill属性的第一条路径(后面的水滴)继承,color属性的值被作为第二条路径(前面的水滴)的fill属性的值。

所以当前颜色值被引用到<use>元素里边,使用currentColor。漂亮整洁,对吗?

这是上面代码的demo:

这种双色变量技术对于简单的双色标志非常好用。在Fabrice的文章中,他创建了三个不同的sass logo,通过改变文本的颜色和背景颜色。

currentColor关键字目前只在CSS变量中支持。但是,如果我们有更多的变量,是不是就可以分配和释放更多的值到<use>内容中呢?对的!Amelia Bellamy-Royds在CodePen blog的文章中介绍了这个概念,大概一年多之前。我们来看看它是如何工作的。

将来:使用CSS自定义属性给<use>内容添加样式,即CSS变量

使用CSS自定义属性(即CSS变量),你可以给<use>的内容添加样式,而不需要强制浏览器覆盖任何描述属性的值。

MDN中定义的,CSS变量可以是作者、或用户,定义的,web页面中包含整个文档中指定的值的实体。它们被设置了使用自定义属性,并通过一个指定的功能符号var()访问。和CSS预处理器(如sass)的变量非常相似,但是更灵活,可以做预处理器变量不能做的事情。(CSS变量很快将被添加到Codrops CSS参考条目中,敬请期待。)

变量,即CSS变量或预处理器变量,都可以有很多使用示例,但是主题(颜色)是最常见的用例之一。在这一节中我们将讲解在给SVG添加样式时如何使用它。

我们先从一张symbol中定义并通过use实例化的图像开始,并且只为这张图像应用这种技术;只要你想,这个示例中给<use>内容应用样式的概念,也可以被应用到很多<use>元素中。

现在,假设我们有如下这个可爱时髦的机器人插画,Freepik设计~

机器人插画

机器人的代码包括了这些组成颜色。

<svg style="display: none">
    <symbol id="robot" viewBox="0 0 340 536">
        <path d="..." fill="#fff" />
        <path d="..." fill="#D1312C" />
        <path d="..." fill="#1E8F90" />
        <path d="..." fill="#1E8F90" />
        <path d="..." fill="#fff" />
        <path d="..." fill="#6A4933" />
        <path d="..." fill="#1E8F90"  />
        <path d="..." fill="#6A4933" />
        <path d="..." fill="#fff" />
        <path d="..." fill="#6A4933" />
        <path d="..." fill="#F2B42B" />
        <path d="..." fill="#fff" />
     
       <!-- rest of the shapes -->
    </symbol>
</svg>

现在,我们不打算使用CSS变量作为每条路径的fill属性的值;我们将使用CSSfill属性作为其填充颜色值,并保留原位置的fill属性。这个属性将作为降级使用,在不支持CSS变量的浏览器中,这样这个图像在那些不支持变量的浏览器中,仍然能保留初始样式。

添加了变量之后,上面的代码变成如下:

<svg style="display: none">
    <symbol id="robot" viewBox="0 0 340 536">
        <path d="..." fill="#fff" />
        <path d="..." fill="#D1312C" />
        <path d="..." fill="#1E8F90" style="fill: var(--primary-color)" />
        <path d="..." fill="#1E8F90" style="fill: var(--primary-color)" />
        <path d="..." fill="#fff" />
        <path d="..." fill="#6A4933" style="fill: var(--tertiary-color)" />
        <path d="..." fill="#1E8F90" style="fill: var(--primary-color)" />
        <path d="..." fill="#6A4933" style="fill: var(--tertiary-color)" />
        <path d="..." fill="#fff" />
        <path d="..." fill="#6A4933" style="fill: var(--tertiary-color)" />
        <path d="..." fill="#F2B42B" style="fill: var(--secondary-color)" />
        <path d="..." fill="#fff" />
       
        <!-- rest of the shapes -->
    </symbol>
</svg>

因为内联style标签会覆盖描述属性,支持CSS变量的浏览器会使用这些变量作为图形的填充颜色。不支持CSS变量的浏览器将使用fill属性值。

下一步,我们需要在CSS中定义变量的值。首先,插画需要使用use实例化:

<svg width="340" height="536">
    <use xlink:href="#robot" id="robot-1" />
</svg>

然后,变量将会定义在use上,这样它们会被级联到内容上。你给变量选的颜色将会构成你的插画内容的颜色主题。所以,对于上面的机器人,构成图形有三种主要的颜色,我把它们命名为primary、secondary和tertiary。

#robot-1 {
  --primary-color: #0099CC;
  --secondary-color: #FFDF34;
  --tertiary-color: #333;
}

有这些变量,你仍然可以使用fillcolor属性,但是你可能不需要或者根本不想要。所以,有了以上通过变量定义的颜色,我们的机器人如下:

机器人插画

你可以根据自己需要创建很多图像的副本,每个都定义一组不同的颜色,然后使用不同的颜色主题。当你想要给同一个logo根据上下文,以不同的方式为其添加样式时,或者任何其它相似的用例。

现在,我们提到那些不支持CSS变量的浏览器会降级到描述属性的初始样式,而不支持变量的浏览器将会使用fill属性来覆盖属性。ok!但如果浏览器不支持CSS变量,而且作者还没有为它们提供指定的变量值,或者他们提供的值是无效的,会发生什么呢?

对于我们这里的时髦可爱的机器人,我们定义了三个变量,只有图像中的一小部分元素没有获取任何变量,因为它们使用默认的颜色,任何颜色的主题都是非常漂亮的。所以,如果你在支持CSS变量的浏览器(目前只有Firefox)中展示上面的代码,然后从CSS中删除变量声明,你将会得到:

机器人插画

如果如果没有设置变量值或者变量值无效,浏览器会使用默认颜色,通常是黑色的填充和描边。

避免的方法是为浏览器支持提供另一种降级颜色。事实上,CSS变量的语法就可以做到这一点:在var()函数内部不要只提供变量值一条声明,你可以用逗号分隔提供两条声明:变量名和一个降级颜色值——在这里就是指我们在描述属性中使用的值。

所以,回到上面的机器人代码,如下:

<svg style="display: none">
    <symbol id="robot" viewBox="0 0 340 536">
        <path d="..." fill="#fff" />
        <path d="..." fill="#D1312C" />
        <path d="..." fill="#1E8F90" style="fill: var(--primary-color, #1E8F90)" />
        <path d="..." fill="#1E8F90" style="fill: var(--primary-color, #1E8F90)" />
        <path d="..." fill="#fff" />
        <path d="..." fill="#6A4933" style="fill: var(--tertiary-color, #6A4933)" />
        <path d="..." fill="#1E8F90" style="fill: var(--primary-color, #1E8F90)" />
        <path d="..." fill="#6A4933" style="fill: var(--tertiary-color, #6A4933)" />
        <path d="..." fill="#fff" />
        <path d="..." fill="#6A4933" style="fill: var(--tertiary-color, #6A4933)" />
        <path d="..." fill="#F2B42B" style="fill: var(--secondary-color, #F2B42B)" />
        <path d="..." fill="#fff" />

         <!-- rest of the shapes -->
    </symbol>
</svg>

就是它了。对任何无法加载或没有定义值的变量,浏览器将会降级使用标签中定义的初始颜色。非常棒!

使用这种技术,你现在可以在页面上任何你想要的地方,使用<use>引用机器人。在CSS中为每一个新实例定义一组变量值,这样每个实例都会有一组不同的颜色主题。

你可以看看上面的demo,创建很多机器人副本,然后指定不同的变量值,只要确保你使用的是Firefox浏览器,因为在写这篇文章的当前(2015.7.16)只有它支持CSS变量:

如果你是在Firefox中查看demo,你会看到我们用CSS变量定义的蓝色+黄色版本的机器人。记得在Chrome中查看其降级机制(绿色版本),然后在Firefox中尝试删除变量声明,看看降级如何。

总结

呼~写了这么长。

利用CSS样式的优势,给<use>的内容添加样式——虽然在shadow DOM中可以不要这么复杂。用CSS变量(仅一个currentColor或自定义属性)我们可以进入到shadow DOM中,定制我们的图形,并提供非常好的降级机制。

就我个人而言,我非常喜欢捣鼓CSS变量+SVG的组合。我喜欢他们组合在一起时非常地强大,特别是考虑到它们强大的降级机制。它们目前只在Firefox中支持,正如我们提到的,但是如果你想要看它们得到更多的支持,你可以在MS Edge User Voice forums上为它们投票。

在将来我们可能会有其它的方法来给use内容添加样式,因为已经有很多这方面的讨论,关于使用CSS变量作为SVG的参数;所以这篇文章,虽然很长,而且可能没有涵盖到所有这方面的内容。如果你有什么其它的想法,请在评论中留言。

处理重用SVG元素的内容已经成为最热门的SVG话题之一,很多人都觉得很难处理,因为克隆的代码在对应的位置如何自然展示。涉及的和相关的话题非常多,但是这是其它文章要讨论的了。

希望你喜欢这篇文章,并能对你有所帮助,感谢阅读!

本文根据@SaraSoueidan的《Styling SVG <use> Content with CSS》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://tympanus.net/codrops/2015/07/16/styling-svg-use-content-css

彦子

在校学生,本科计算机专业。逗比一枚,热爱前端热爱生活,喜欢CSS喜欢JavaScript喜欢SVG,爱玩PS玩AI玩啊逗比的软件。努力向上,厚积薄发。

如需转载,烦请注明出处:https://www.fedev.cn/svg/styling-svg-use-content-css.htmlnike free run 5.0 yellow