With the spirit of using new widely supported, Baseline CSS rules, I’m implementing light-dark() as a simplified method to allow light and dark mode on a website. This function is a game changer, very simple to apply color modes for websites, so I’m a big fan already. That said, using SVG images as background images exposes an edge case that took me down a rabbit hole leading me to CSS masks.

Let’s apply light-dark using color-scheme for the site.

:root {
  /* NOTE: Use @supports for <= Safari 17.4 (2024-05) */
  @supports (color: light-dark(black, white)) {
    color-scheme: light dark;
	  --surface-color: light-dark( #ccc, #333 );
	  --text-color: light-dark( #333, #ccc );
  }
}

I’m applying carets to navigation menu item links, which look like this:

navigation item link named "Resources" with down-pointing caret on the right of the link

So that chevron or caret on the right side of Resources, which is pointing down, is applied to the anchor element as a background image.

a {
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' [...]");
}

Why am I adding an SVG as a background image here? Wouldn’t it be easier to add SVG to the source code? Well, yes, but this is WordPress, and since I’m pulling in the navigation menu from an API, the markup is auto-generated and formatted so it’s not easy to add in additional markup into the API results. I’m sure we’ve all faced similar limitations on generated code from an API, so I was determined to attack this problem and find a solution.

Additionally, I’m not referencing an additional file as a way to save an HTTP request, one of the many micro-optimizations for better performance. This also gives us the ability to easily cache the image, or if I decide to use an SVG sprite in the future, this saves multiple requests.

Back to the code above, see that fill='currentColor' rule? It doesn’t work as you would expect because, regardless of the SVG being added a background image, the SVG is being treated as an external resource. Considering this, we can’t apply the currentColor value to inherit the anchor’s color.

In the Stackoverflow I just linked to above, there’s a reference to Coloring SVGs in CSS Background Images, which provided me a big clue to solve this, which is using CSS masks. It worked… kinda!

I converted everything from background to be mask, because the syntax is mostly identical, and added a background color for the mask to work correctly. Well, that actually hid the text of the anchor element, so while I was now able to see the background caret with the correct color mode text color, the actual text was hidden. To this point, I’ve purposely been trying to not use ::after pseudo-elements out of principle, just to make things a little simpler. Time to break through the ego and add back that pseudo-element!

The final code that works for me:

a::after {
  background-color: var(--text-color);
  content: "";
  height: 100%;
  mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' aria-hidden='true' viewBox='0 0 20 20'><path fill-rule='evenodd' d='M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z' clip-rule='evenodd'/></svg>");
  mask-position: left 17%;
  mask-repeat: no-repeat;
  mask-size: 0.9rem;
  position: absolute;
  width: 100%;
}

Problem solved!