Can I have an SVG's CSS Stroke property inherit a color from the SVG markup?

Viewed 322

Edit: In my case, I need IE11 support, so CSS variables aren't the way to go. If you can use CSS variables, see Kaiido's answer. If not, see Paul's answer.


I've got a simple website with the concept of "light" and "dark" themes. Light themes are white or very light backgrounds, and dark themes are some darker color. Most components on the website work by adjusting their color based on their position inside elements that have these themes. For example:

.dark-theme {
  background-color: black;
}

.light-theme  {
  background-color: white;
}

/* When the paragraph is inside a dark theme */
.dark-theme p,
.light-theme .dark-theme p {
  color: white;
}

/* When the paragraph is inside a light theme */
.light-theme p,
.dark-theme .light-theme p {
   color: black;
}

.dark-theme, .light-theme {
    padding: 10px;
}
<div class="light-theme">
  <p>Light Theme</p>
  <div class="dark-theme">
    <p>Dark Theme</p>
  </div>
</div>
<div class="dark-theme">
  <p>Dark Theme</p>
  <div class="light-theme">
    <p>Light Theme</p>
  </div>
</div>

This allows us to have each component just define how it needs to display in dark vs. light theme, and its placement in the DOM will take care of the rest.

The catch here is that we must actually write definitions for each case (dark on light, and light on dark). If we don't define one case, then we will get incorrect behaviour when the elements are nested.

However, I cannot get this to work with SVGs in a specific scenario where the SVG has a stroke defined as an HTML attribute. We are using icons that come from somewhere else so adjusting the SVG markup is not an option.

What we want is: when an SVG is on a light background (.light-theme), the SVG should be whatever color is in the SVG markup (there may be more than one). Whenever an SVG is on a dark background (.dark-theme), we want it to be white (working).

Below is what happens when we try the same thing as we did above with the <p> using the stroke or fill properties of the SVG. It seems I cannot use unset, inherit, or initial to get it to fall back to the SVG's actual value. Is this expected?

And if I just don't define the behaviour for the light theme, it will fail in cases where the themes are nested.

So, the tl;dr is: how can I override stroke and fill properties that were set using CSS with a more specific CSS rule to get an SVG to use its original color that was defined in its markup?

/* When the paragraph is inside a dark theme */
.dark-theme p,
.light-theme .dark-theme p {
  color: white;
}

/* When the paragraph is inside a light theme */
.light-theme p,
.dark-theme .light-theme p {
   color: black;
}

/* When the circle is inside a dark theme */
.dark-theme svg circle,
.light-theme .dark-theme svg circle {
  stroke: white;
  fill: white;
}

/* When the circle is inside a light theme */
.light-theme svg circle,
.dark-theme .light-theme svg circle {
   stroke: initial; /* How to make the circle red here? */
   fill: initial; /* How to make the circle red here? */
}

/** helpers below here **/

svg {
  display: block;
  margin: 0 auto;
}

.light-theme, .dark-theme {
  padding: 10px;
}

.dark-theme {
  background-color: black;
}

.light-theme  {
  background-color: white;
}
<div class="light-theme">
  <p>Light Theme</p>
  <div class="dark-theme">
    <p>Dark Theme</p>
    <svg height="100" width="100">
      <circle cx="50" cy="50" r="40" stroke="red" stroke-width="3" fill="red" />
    </svg> 
  </div>
</div>
<div class="dark-theme">
  <p>Dark Theme</p>
  <div class="light-theme">
    <p>Light Theme</p>
    <svg height="100" width="100">
      <circle cx="50" cy="50" r="40" stroke="red" stroke-width="3" fill="red" />
    </svg> 
  </div>
</div>

By request: here's an example of how it could be nested. There are essentially no DOM constraints we can rely on.

<div class="light-theme"> <!-- nesting can go light > dark > light > dark, etc -->
  <div class="dark-theme">
    <p>Dark Theme</p>
    <div class="something anything really"> <!-- this may or may not be here -->
      <div class="another container dark-theme"> <!-- this may or may not be here -->
        <div class="light-theme">
          <p>Light Theme</p>
          <svg height="100" width="100">
            <circle cx="50" cy="50" r="40" stroke="red" stroke-width="3" fill="red" />
          </svg> 
        </div>
      </div>
    </div>
  </div>
</div>

2 Answers

The stroke and fill attributes map directly to CSS properties, with less specificity than any true (not inherited) CSS property.

We should be able to do what you want easily through the attr() CSS function, which would allow us to duplicate that rule with an higher specificity, but it's still not yet supported anywhere.

One possible workaround, since you are willing to recolorize your SVGs entirely to white, is to use filters to do so, instead of changing the fill and stroke. This way, we can force a filter:none when we are in light-mode.

Note that your current rule actually fails, even for the text's color, in cases like .dark-theme > .light-theme > .dark-theme since both rules have the same specificity. To fix this, we have to rely on inheritance instead, and for filter that means using CSS-variables.

.dark-theme {
  color: white;
  --filter: brightness(0) grayscale(1) invert(1);
}
.light-theme {
   color: black;
   --filter: none;
}
svg circle {
  filter: var(--filter);
}

.dark-theme {
  color: white;
  --filter: brightness(0) grayscale(1) invert(1);
}
.light-theme {
   color: black;
   --filter: none;
}
svg circle {
  filter: var(--filter);
}

/** helpers below here **/

svg {
  display: block;
  margin: 0 auto;
}

.light-theme, .dark-theme {
  padding: 10px;
}

.dark-theme {
  background-color: black;
}

.light-theme  {
  background-color: white;
}
<div class="light-theme">
  <p>Light Theme</p>
  <div class="dark-theme">
    <p>Dark Theme</p>
    <svg height="100" width="100">
      <circle cx="50" cy="50" r="40" stroke="red" stroke-width="3" fill="red" />
    </svg> 
  </div>
</div>
<div class="dark-theme">
  <p>Dark Theme</p>
  <div class="light-theme">
    <p>Light Theme</p>
    <svg height="100" width="100">
      <circle cx="50" cy="50" r="40" stroke="red" stroke-width="3" fill="red" />
    </svg> 
  </div>
</div>
<div class="light-theme"> <!-- nesting can go light > dark > light > dark, etc -->
  <p>Light Theme</p>
  <div class="dark-theme">
    <p>Dark Theme</p>
    <div class="something anything really"> <!-- this may or may not be here -->
      <div class="another container dark-theme"> <!-- this may or may not be here -->
        <div class="light-theme">
          <p>Light Theme</p>
          <svg height="100" width="100">
            <circle cx="50" cy="50" r="40" stroke="red" stroke-width="3" fill="red" />
          </svg> 
        </div>
      </div>
    </div>
  </div>
</div>
<div class="light-theme"> <!-- nesting can go light > dark > light > dark, etc -->
  <p>Light Theme</p>
  <div class="dark-theme">
    <p>Dark Theme</p>
    <div class="something anything really"> <!-- this may or may not be here -->
      <div class="another container dark-theme"> <!-- this may or may not be here -->
        <div class="light-theme">
          <p>Light Theme</p>
          <div class="dark-theme">
            <p>Dark Theme</p>
            <svg height="100" width="100">
              <circle cx="50" cy="50" r="40" stroke="red" stroke-width="3" fill="red" />
            </svg> 
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Only apply the dark theme colours to the SVG when it is a direct child of the div.dark-theme.

.dark-theme > svg circle {
  stroke: white;
  fill: white;
}

a > b means apply this rule if b is one of the children of a.

If you potentially have deeper nesting, you could expand this rule like follows:

.dark-theme > svg circle,
.dark-theme > :not(.light-theme) > svg circle,
.dark-theme > :not(.light-theme) > :not(.light-theme) > svg circle,
.dark-theme > :not(.light-theme) > :not(.light-theme) > :not(.light-theme) > svg circle,
.dark-theme > :not(.light-theme) > :not(.light-theme) > :not(.light-theme) > :not(.light-theme) > svg circle
{
  stroke: white;
  fill: white;
}

It's perhaps a bit ugly, but it works and is simple. :)

Demo:

/* When the paragraph is inside a dark theme */
.dark-theme p,
.light-theme .dark-theme p {
  color: white;
}

/* When the paragraph is inside a light theme */
.light-theme p,
.dark-theme .light-theme p {
   color: black;
}

/* When the circle is inside a dark theme */
.dark-theme > svg circle,
.dark-theme > :not(.light-theme) > svg circle,
.dark-theme > :not(.light-theme) > :not(.light-theme) > svg circle,
.dark-theme > :not(.light-theme) > :not(.light-theme) > :not(.light-theme) > svg circle,
.dark-theme > :not(.light-theme) > :not(.light-theme) > :not(.light-theme) > :not(.light-theme) > svg circle
{
  stroke: white;
  fill: white;
}

/** helpers below here **/

svg {
  display: block;
  margin: 0 auto;
}

.light-theme, .dark-theme {
  padding: 10px;
}

.dark-theme {
  background-color: black;
}

.light-theme  {
  background-color: white;
}
<div class="light-theme">
  <p>Light Theme</p>
  <div class="dark-theme">
    <p>Dark Theme</p>
    <div class="foo">
      <svg height="100" width="100">
        <circle cx="50" cy="50" r="40" stroke="red" stroke-width="3" fill="red" />
      </svg>
    </div>
  </div>
</div>
<div class="dark-theme">
  <p>Dark Theme</p>
  <div class="light-theme">
    <p>Light Theme</p>
    <svg height="100" width="100">
      <circle cx="50" cy="50" r="40" stroke="red" stroke-width="3" fill="red" />
    </svg> 
  </div>
</div>

<!-- UPDATE -->
<div class="light-theme"> <!-- nesting can go light > dark > light > dark, etc -->
  <div class="dark-theme">
    <p>Dark Theme</p>
    <div class="something anything really"> <!-- this may or may not be here -->
      <div class="another container dark-theme"> <!-- this may or may not be here -->
        <div class="light-theme">
          <p>Light Theme</p>
          <svg height="100" width="100">
            <circle cx="50" cy="50" r="40" stroke="red" stroke-width="3" fill="red" />
          </svg> 
        </div>
      </div>
    </div>
  </div>
</div>

Related