Skip to content
Axialis Developer Network

Guide

Themeable icons with currentColor & dark-mode variants

Use currentColor and CSS custom properties to make icons recolor with your theme, then build legible dark-mode variants that pass WCAG AA contrast.

Updated

  • #currentcolor
  • #dark-mode
  • #css
  • #wcag
  • #contrast
  • #accessibility
  • #icon-theming

An icon that only exists in one color is a liability. It will look wrong on dark backgrounds, conflict with brand recolors, and break when a user enables high-contrast mode. Building icons that respond to the surrounding CSS context — rather than hardcoding a specific color — is not a luxury feature. It is the baseline for any component library or design system that will be used in more than one environment.

This guide covers currentColor, CSS-driven recoloring, what it actually means to build a legible dark-mode variant (versus just inverting the colors), contrast requirements, prefers-color-scheme, and the cases where a separate dark asset genuinely outperforms a CSS-only approach.

How currentColor works

currentColor is a CSS keyword that resolves to the value of the color property on the element itself (or the nearest ancestor that sets color). When an SVG path has fill="currentColor", setting color: #2563EB on a parent element turns that icon blue — no class swap, no duplicate SVG file.

<!-- The icon inherits whatever color the button text uses -->
<button class="btn-primary">
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2"
          stroke-linecap="round" stroke-linejoin="round"/>
  </svg>
  Continue
</button>
.btn-primary {
  color: #ffffff; /* both the label AND the icon become white */
}
.btn-secondary {
  color: #1e40af; /* both become blue */
}

The key requirement is that the SVG paths use stroke="currentColor" or fill="currentColor" — not hardcoded hex values. If your SVG exports contain fill="#111827", they need to be cleaned before this works. See the guide on cleaning SVGs before you ship for the full process.

currentColor in inlined SVGs vs <img> and <use>

currentColor only works when the SVG is in the same CSS cascade as the surrounding content:

Embed method currentColor works? Notes
Inline <svg> in HTML Yes Full CSS access; also allows :hover on child paths
<use href="sprite.svg#icon"> (external) Partial Works if the external SVG is on the same origin; blocked cross-origin
<use href="#icon"> (inline sprite) Yes The <symbol> and its <use> share the same document
<img src="icon.svg"> No SVG is rendered in isolation; CSS from the page cannot reach it
CSS background-image: url("icon.svg") No Same isolation as <img>
object-fit / <object> No Separate browsing context

If your architecture requires <img> or CSS background for SVG icons (common in content-managed sites), you need a separate file per color state. There is no workaround — that is simply how the browser security model works.

CSS-driven recoloring with custom properties

currentColor handles single-color icons. For icons with two distinct tones — a primary shape and a secondary accent — CSS custom properties give you named color slots that can be controlled from outside the SVG:

<!-- dual-tone-star.svg -->
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <!-- Primary shape uses currentColor -->
  <path fill="currentColor" d="M12 2l3.09 6.26L22 9.27l-5 4.87..."/>
  <!-- Badge/accent uses a custom property with currentColor as fallback -->
  <circle fill="var(--icon-accent, currentColor)" cx="18" cy="6" r="4"/>
</svg>
/* Default: both parts use the text color */
.icon-star { color: #374151; }

/* Highlighted: primary stays gray, accent goes amber */
.icon-star--highlighted {
  color: #374151;
  --icon-accent: #f59e0b;
}

/* In a dark context: both adapt */
.dark .icon-star { color: #d1d5db; }
.dark .icon-star--highlighted {
  color: #d1d5db;
  --icon-accent: #fbbf24;
}

CSS custom properties cascade through the shadow DOM in supported browsers, so this pattern also works in Web Components.

Dark mode: not just inversion

The most common mistake when adding dark-mode icon support is to invert the colors — turning #111827 (near-black) into #e7e8fd (near-white). For a plain line icon against a plain background this looks acceptable, but it falls apart in practice:

  • Colored icons invert to unexpected hues. An orange warning icon inverts to an electric blue that reads as an entirely different semantic meaning.
  • Multi-tone icons where two values happen to produce similar luminance end up looking flat or muddy after inversion.
  • Subtle icons (low-contrast decorative elements in light mode) become harshly bright spots in dark mode, destroying the visual hierarchy.

The right approach is to define explicit dark-mode values — but those values do not need to be maintained as separate SVG files. They can be CSS values that override the light-mode color tokens.

Designing for legibility, not symmetry

A light-mode icon that works at #374151 (a medium-dark gray, ~11:1 contrast on white) does not need to become #c8cad0 (the exact luminance mirror) in dark mode. It needs to be legible against the dark background and to carry the same visual weight.

In practice, icons on dark backgrounds often use slightly higher saturation and lighter values than pure luminance math would suggest — because dark screens emit light rather than reflect it, and the human eye perceives saturation differently at high luminance ratios.

A practical set of contrast-checked dark-mode icon values, using #111827 (WCAG dark background approximation) as the surface:

Role Light-mode token Dark-mode token Contrast on surface (approx.)
Default icon #374151 (gray-700) #d1d5db (gray-300) ~10:1 on #111827
Muted / decorative #9ca3af (gray-400) #6b7280 (gray-500) ~4.8:1 on #111827
Brand accent #2563eb (blue-600) #60a5fa (blue-400) ~5.2:1 on #111827
Destructive / error #dc2626 (red-600) #f87171 (red-400) ~4.6:1 on #111827
Warning #d97706 (amber-600) #fbbf24 (amber-400) ~7.1:1 on #111827
Success #059669 (green-600) #34d399 (green-400) ~6.8:1 on #111827

WCAG 2.1 Level AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text and graphical components (including icons used to convey meaning). Every value in the table above meets the 4.5:1 bar, which gives headroom for the icon to be used in varying contexts.

Using prefers-color-scheme

prefers-color-scheme is a CSS media query that reads the user's OS-level dark/light preference. It has broad browser and OS support — modern versions of Chrome, Firefox, Safari, and Edge on both desktop and mobile all honour it.

The cleanest implementation for icon theming uses CSS custom properties defined at the :root level, overridden within the media query:

/* tokens.css */
:root {
  --color-icon-default: #374151;
  --color-icon-muted: #9ca3af;
  --color-icon-accent: #2563eb;
  --color-icon-error: #dc2626;
  --color-icon-warning: #d97706;
  --color-icon-success: #059669;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-icon-default: #d1d5db;
    --color-icon-muted: #6b7280;
    --color-icon-accent: #60a5fa;
    --color-icon-error: #f87171;
    --color-icon-warning: #fbbf24;
    --color-icon-success: #34d399;
  }
}

Then any icon component simply uses the token:

<svg viewBox="0 0 24 24" style="color: var(--color-icon-default);" aria-hidden="true">
  <path fill="currentColor" d="..."/>
</svg>

Because currentColor picks up the color property, setting color: var(--color-icon-default) on the SVG (or a parent) is all that is needed. The tokens swap at the prefers-color-scheme breakpoint; no JavaScript required.

User-overridable theme toggle

Most modern sites also want a manual toggle that overrides the OS preference. The standard pattern is to store the user's choice in localStorage and apply a data-theme attribute to the <html> element:

:root[data-theme="dark"] {
  --color-icon-default: #d1d5db;
  /* ... etc */
}

:root[data-theme="light"] {
  --color-icon-default: #374151;
  /* ... etc */
}

When data-theme is present it takes precedence. When absent, prefers-color-scheme controls behavior automatically.

When currentColor is not enough — separate dark assets

currentColor with CSS tokens handles the overwhelming majority of icon use cases. There are a handful of situations where a separate dark-mode SVG file is the more practical choice:

Illustrations with gradients

currentColor does not work with <linearGradient> or <radialGradient> stop colors. A gradient-based illustration icon that needs to change from a warm amber gradient in light mode to a cooler blue-gray in dark mode requires two separate files — or a more complex CSS filter approach that can be hard to maintain.

Optimized fill-area icons vs outline icons

Some icon sets ship both a filled variant (used in dark mode where filled forms have better legibility against dark backgrounds) and an outline variant (used in light mode). This is a design-system convention, not a technical limitation of currentColor. If your icon set follows this convention, you are managing two separate SVG sets, and the switching logic is typically a class on the icon component (icon--filled / icon--outline) rather than CSS variables.

Bitmap icons that exist in PNG format

Any time your icons are PNG rather than SVG, CSS color control is off the table entirely. PNG pixels are pixel values; CSS cannot change them at render time. For these cases you have two options: maintain separate light and dark PNG files, or use CSS filter: invert(1) as a rough approximation. The filter approach fails for colored icons for the same reason pure inversion fails generally.

This is one of the strongest arguments for keeping your icon source in SVG. For a deeper discussion of when to use SVG versus bitmap formats, see the guide on SVG vs PNG vs ICO vs ICNS vs WebP.

Contrast checklist before shipping

Before a dark-mode icon variant goes to production, verify each of the following:

  1. AA contrast — the icon color achieves at least 4.5:1 against the intended background (3:1 if the icon is large, or decorative only).
  2. Not inversion — the dark variant was chosen by a designer, not produced by a CSS filter: invert(1).
  3. Semantic color preservation — red (error), amber (warning), and green (success) icons remain clearly distinguishable in dark mode, including for users with red-green color deficiency. Do not rely solely on hue.
  4. Focus ring visibility — if the icon is inside an interactive control, the focus ring is visible in both modes.
  5. High-contrast mode — test with Windows High Contrast / Forced Colors mode enabled. In Forced Colors, browsers replace custom properties with system colors, which is usually the right behavior if you have used semantic tokens rather than hardcoded values.

For the broader workflow of generating and exporting icon variants for different targets, including light/dark, see the guide on exporting icons to React, Vue, SwiftUI, and WPF.