← All writing
Craft · · 6 min

currentColor

On the keyword that's been in CSS since before I started, the properties that use it whether you ask or not, and the icon problem it quietly solves.

CSS

The component, abbreviated:

button.css, before
.button--danger {
  color: #b3261e;
  border-color: #b3261e;
  text-decoration-color: #b3261e;
}
.button--danger .icon { fill: #b3261e; }
.button--danger:focus-visible { outline-color: #b3261e; }
CSS

Times four variants. 40-odd declarations, all of them the same value repeated, and every one a place where a brand change means a find-and-replace that misses one.

button.css, after
.button--danger { color: #b3261e; }
CSS

That’s the whole post, really, but the reasons are worth spelling out because currentColor is one of the oldest things in CSS and I meet people who’ve never used it deliberately.

The properties that already default to it

The bit that surprised me when I actually went and checked.

border-color, if you don’t set it, is the element’s color. So border: 1px solid with no color is a border in the text color, and it always has been.

outline-color likewise, in current browsers, which means a focus ring picks up the text color without being told.

text-decoration-color the same, which is why underlines match their text.

box-shadow without a color uses the current color. So does column-rule-color, and the caret in an input.

Which means a substantial proportion of the color declarations in any stylesheet are restating a default. Not wrong, exactly, and each one is a place the value can drift out of step with the others.

A good proportion of the color declarations in any stylesheet are carefully restating the default, and each restatement is a place the value can drift.

Set one, get six

The pattern that makes it worth using deliberately rather than just relying on the defaults: a component’s entire color treatment derived from one property.

button.css
.button {
  border: 1px solid;                 /* currentColor */
  box-shadow: 0 1px 0;               /* currentColor */
}
.button .icon      { fill: currentColor; }
.button:hover      { color: var(--accent-strong); }   /* all of it follows */
.button:disabled   { color: var(--text-disabled); }   /* and again */

.button--danger    { color: var(--danger); }
.button--quiet     { color: var(--text-muted); }
CSS

Every state and every variant is one declaration. The hover state doesn’t need to restate the border, the disabled state doesn’t need to restate the icon, and adding a fifth variant is a line.

It also means the component behaves correctly in contexts you didn’t anticipate. Drop it inside a panel that sets a different text color and the whole thing adapts, because it was never carrying a hardcoded color to begin with.

Icons, which is the real win

The case I’d single out, and it’s the answer to a problem I complained about in 2019 when I was doing dark mode and getting cross about images.

An inline SVG whose paths use fill="currentColor" takes the text color of wherever it sits.

icon.svg
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
  <path fill="currentColor" d="…"/>
</svg>
HTML

So an arrow inside a link is the link color. On hover it changes with the link. In dark mode it changes with the theme. Inside a disabled button it goes gray. On an inverted panel it inverts. Inside a danger variant it goes red.

None of that needs a single rule targeting the icon, and every one of those cases is a rule I have written by hand, repeatedly, on every project, since about 2013. It’s also the fix for the thing I gave up on six years ago, where a themed site has supplied assets that don’t follow the theme.

I now ask for icons as SVG with no fill or with currentColor, as a matter of course, and I’d put it in a design handover checklist if I had one.

Where it doesn’t reach

An SVG loaded through <img> or as a CSS background. That’s a separate document and your page’s CSS doesn’t apply to it, so currentColor inside it resolves against the SVG’s own root, which is black. This catches everybody once.

The workarounds: inline the SVG, use a sprite with <use>, which does inherit correctly, or use the mask approach, where the SVG becomes a mask-image and the color comes from background-color.

masked.css
.icon {
  background-color: currentColor;
  mask-image: url("/icons/arrow.svg");
  mask-size: contain;
  mask-repeat: no-repeat;
}
CSS

That gives you a single-color icon from an external file that follows the text color, which is a nice trick and does mean the icon can only be one color.

And it’s the element’s own color. currentColor on a child resolves against the child’s computed color, not the parent’s. Which is nearly always what you want and is occasionally confusing when something in between has set a color you’d forgotten about.

color-mix() extends it properly

The newer addition that makes this genuinely powerful, and it’s in every browser now.

derived.css
.callout {
  border: 1px solid color-mix(in oklch, currentColor 30%, transparent);
  background-color: color-mix(in oklch, currentColor 8%, transparent);
}
CSS

A tinted panel whose border and background are derived from its text color. Set color and you get a coordinated three-color component. Change color and all three move together, in step, permanently.

That’s the thing I used to do with Sass color functions at build time, and it now happens at runtime, which means it works with a theme toggle and with whatever color a client picks in an admin panel. Which was the argument for custom properties back in 2016 and it’s the same argument arriving somewhere new.

Older than my career

The component went from about 40 color declarations to four.

What I find slightly funny is that currentColor predates essentially everything else I’ve written about here. It’s older than flexbox, older than custom properties, older than my career. It has been sitting there the whole time being the cheapest theming primitive in the language, and I spent ten years typing hex codes next to it.

Read similar posts
6 min

Style queries

Five surface variants, four child elements, and 20 selectors that existed only to tell a child which parent it was in. Style queries delete the whole grid of them. That isn't quite the same as saying you needed them.

6 min

text-wrap: balance

A card grid where, at exactly one breakpoint, one heading broke with a single word on the second line, and the fix used to be a non-breaking space typed into the content by a person who'd have to remember forever.