Controlling SVG colors with CSS

2020-02-07 11:35发布

问题:

I'm setting up a few icons in a library that's using some basic CSS and an SVG Sprite (generated through webpack).

Some of the icons I want to be able to color with multiple colors. My set up looks like:

mail.svg (details of the svg are omitted for simplicity)

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 64 64" width="64" height="64">
  <polyline class="primary-stroke" fill="none" stroke-width="2" [more-stuff-here]></polyline>
  <path fill="none" stroke-width="2" [more-stuff-here]></path>
  <line class="primary-stroke" fill="none" stroke-width="2" [more-stuff-here]></line>
</svg>

My computed CSS (blue is the primary accent color) looks like:

svg {
  fill: currentColor;
  stroke: currentColor;
}

.primary-stroke {
  stroke: blue;
  fill: none;
}

And my HTML looks like:

<svg><use xlink:href="#mail"></svg>

This all works exactly as expected, but now I want to take it a step further. I want to be able to add a class to the element to determine if this instance should contain 1 single color or 2 colors.

My attempt was pretty simple. I just added a single-color class to the svg element to look like:

<svg class="single-color"><use xlink:href="#mail"></svg>

And modified the SCSS. The computed CSS looks like:

.single-color .primary-stroke {
  stroke: currentColor;
  fill: none;
}

But, it definitely does not work. The primary styles still take effect. I'm new to working with SVGs and I'm not sure if what I'm trying to do is even possible with a sprite?

CodePens demonstrating the issue:

  • Working Demo (no Sprites): https://codepen.io/amlyhamm/pen/ddjXBp

  • Not Working (using Sprites): https://codepen.io/amlyhamm/pen/paZbMq

Both examples use the same classes and SVGs.

回答1:

The element referenced by the <use> element is not per se part of the DOM chain, it is only present in the shadow DOM, and hence, you can't access it through your selector.

The solution would be to target directly the <use> element itself, and don't set rules for the inner .primary-stroke so that they can inherit from the <use>.

/* 
  don't set any direct rule on the .variable ones 
  otherwise, they won't be able to inherit from the <use>
*/

/* target the uses */
.stroke-only use[href="#rects"] {
  stroke: blue;
  fill: none;
}
.stroke-and-fill use[href="#rects"] {
  stroke: blue;
  fill: green;
}

/* this one won't get influenced by the <use> */
.fixed {
  fill: orange;
  stroke: red;
}

svg { display: block; }
<svg width="0" height="0" style="position:absolute;z-index:-1">
  <defs>
    <g id="rects">
      <rect class="variable" x=5 y=5 width=50 height=50 />
      <rect class="fixed" x=60 y=5 width=50 height=50 />
    </g>
  </defs>
</svg>

<svg class="stroke-only" height=70 >
  <use href=#rects />
</svg>

<svg class="stroke-and-fill" height=70 >
  <use href=#rects />
</svg>


And then for the example in your codepen, you'll need to add specific rules for the one path that doesn't change color e.g:

#mail path:not([class]) {
  stroke: currentColor;
  fill: none;
}

updated codepen.

But the best would be to mark it with a class (like I did with .fixed) if you have control over this sprite-sheet.



回答2:

Depending on what browsers you need to support - I would use CSS Variables they take away all the hacks and pains of trying to control colors in SVGs and they works in the shadow DOM too.

1) Add the variables in your SVG as styles with a fallback for older browsers

<path style="fill: var(--color-name, #8d5000)" fill="#8d5000" d="M...." />
                   CSS variable      Default         Fallback 

2) Include your SVG

<svg class="my-class"><use xlink:href="#resource"></svg>

3) Redefine the colors in your CSS

.my-class {
    --color-one: pink;
    --color-two: magenta;
}  

4) That's it :-)

Example on codepen

[

Support: https://caniuse.com/#feat=css-variables



回答3:

There is only one "currentColor" so you can only change one thing that way. However you can take advantage of the fact that elements can have a stroke and/or a fill to get two colour choices. (This is the logical extension of @Kaiido's example).

Convert the strokes you want to be a different colour into filled shapes. Leave the rest of the elements in stroked line form. Now you can set some of the elements to one colour using stroke, and the rest using fill.

In thie following example, the top line is a <line>. We set its colour using the stroke attribute. The bottom line is actually a rectangle the same shape as the line. We set its colour using the fill attribute.

body {
  color: #aaa;
  display: flex;
  justify-content: center;
  text-align: center;
  font-family: Arial;
}

.example {
  padding: 10px 20px;
}

.example {
  stroke: green;
  fill: blue;
}

.single-color {
  stroke: currentColor;
  fill: currentColor;
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0" id="__SVG_SPRITE_NODE__">
  <symbol viewBox="0 0 64 64" id="mail">
    <!-- The top line is an actual line. -->
    <!-- We set its colour using the stroke attribute. -->
    <line x1="10" y1="20" x2="54" y2="20" stroke-width="16" fill="none"/>
    <!-- The bottom line is a path the same shape as the line -->
    <!-- We set its colour using the fill attribute. -->
    <path d="M 10,35 h44 v16 h-44 z"                        stroke="none" />
</symbol>
</svg>


<div class="example">
  <h2>Two Colors:</h2>

  <svg><use xlink:href="#mail"/></svg>
  
</div>

<div class="example">
  <h2>Single Color:</h2>

   <svg><use xlink:href="#mail" class="single-color"/></svg>
</div>

This approach will work for your example icon, and hopefully for the rest of your icons. But it will not work for every icon. Obviously it relies on one of the colours being representable in stroke form. If your icon was, say, two different coloured blobs, you may not be able to use this technique.