SVG Icons in Dark Mode: The Complete Tailwind & CSS Guide

Three strategies for making SVG icons work correctly in dark mode with Tailwind CSS and plain CSS — currentColor, filter invert, and CSS variables — with code and failure cases.

Amit Yadav
Amit Yadav

Dark mode breaks SVG icons in ways that images never do. A JPEG fades gracefully; a hardcoded fill="#1a1a1a" icon turns invisible against a dark background. The fix is simple once you understand why it fails — but most guides skip straight to the band-aid without explaining the root cause.

This guide covers all three strategies in order of correctness, with the exact scenarios where each one breaks.

Why SVG icons behave differently in dark mode

SVG elements are part of the DOM, not raster images. Their colors are set by attributes (fill, stroke) or CSS properties — which means they participate in the cascade, respond to currentColor, and inherit from parent elements.

The problem is that most raw SVG files from Figma, icon sites, or AI tools come with hardcoded color values baked in:

<!-- Breaks in dark mode — color is hardcoded -->
<svg viewBox="0 0 24 24">
  <path fill="#111827" d="M12 2 ..."/>
</svg>

Changing the surrounding background to dark does nothing to this path’s fill. The icon stays #111827 — invisible on a dark surface.

The one rule that prevents most dark mode icon bugs

Every SVG icon you write or import should use currentColor for its fill and/or stroke. This makes the icon inherit the CSS color property of its parent — which Tailwind’s text-* utilities and dark: variant control perfectly.

Strategy 1: currentColor (correct approach)

currentColor is an SVG/CSS keyword that resolves to the computed value of the color property on the element. Lucide, Heroicons, Tabler, and Phosphor all use it by default — which is the main reason these libraries work in dark mode without any extra configuration.

// Lucide: works in dark mode automatically
import { Moon, Sun } from 'lucide-react';

<div className="text-gray-900 dark:text-gray-100">
  <Moon size={20} />
</div>

The dark:text-gray-100 class sets color: rgb(243 244 246) in dark mode. The Lucide <svg> inherits that color through currentColor on its paths.

For inline SVGs you write yourself:

<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
  <circle cx="12" cy="12" r="10"/>
  <path d="M12 8v4l3 3"/>
</svg>

And in React with Tailwind:

<svg
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  strokeWidth={2}
  className="w-5 h-5 text-gray-700 dark:text-gray-300"
>
  <circle cx="12" cy="12" r="10"/>
</svg>

currentColor with fill-based icons

For filled icons (no stroke, paths use fill):

<svg viewBox="0 0 24 24" className="w-5 h-5 fill-current text-gray-700 dark:text-gray-300">
  <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/>
</svg>

Tailwind’s fill-current sets fill: currentColor, making the path inherit the text color.

Strategy 2: CSS filter: invert() (quick fix — limited use)

If you have icons you can’t edit (locked SVG files, third-party img tags), filter: invert(1) flips all colors. It works in a binary light/dark world.

/* Plain CSS */
.dark img.icon {
  filter: invert(1);
}
/* Tailwind */
<img src="/icon.svg" className="w-5 h-5 dark:invert" alt="" aria-hidden="true" />

Tailwind ships dark:invert as a utility since v3. It’s one class and it works for simple black-on-white icons.

Where invert breaks

Colored icons. Invert flips all channels. A blue icon (#2563eb) becomes orange (#d49714). Never use invert on colored SVGs.

Mid-gray icons. A #888888 icon inverts to #777777 — nearly the same color. The icon may stay invisible on mid-gray dark backgrounds.

Brand icons with specific colors. GitHub’s Octocat, TypeScript’s blue — invert destroys them.

invert is a band-aid, not a solution

Use dark:invert only for single-color decorative icons served as <img> tags where you cannot change the source SVG. For any icon library or inline SVG, use currentColor instead.

Strategy 3: CSS variables for themed icon colors

For design systems that need more granularity than “inherit parent text color,” CSS custom properties give you full control without hardcoding.

Define semantic icon color tokens

/* src/styles/global.css */
:root {
  --icon-default: theme(colors.gray.700);
  --icon-muted:   theme(colors.gray.400);
  --icon-accent:  theme(colors.blue.600);
  --icon-danger:  theme(colors.red.600);
  --icon-success: theme(colors.green.600);
}

.dark {
  --icon-default: theme(colors.gray.300);
  --icon-muted:   theme(colors.gray.500);
  --icon-accent:  theme(colors.blue.400);
  --icon-danger:  theme(colors.red.400);
  --icon-success: theme(colors.green.400);
}

Use the tokens in Tailwind with arbitrary values

<Search className="w-5 h-5 [color:var(--icon-default)]" />
<Bell   className="w-5 h-5 [color:var(--icon-muted)]" />
<AlertCircle className="w-5 h-5 [color:var(--icon-danger)]" />

Or extend your Tailwind config to expose them as utilities:

// tailwind.config.ts
export default {
  theme: {
    extend: {
      colors: {
        'icon-default': 'var(--icon-default)',
        'icon-muted':   'var(--icon-muted)',
        'icon-accent':  'var(--icon-accent)',
        'icon-danger':  'var(--icon-danger)',
      },
    },
  },
};

Then use normally:

<Search className="w-5 h-5 text-icon-default" />

This pattern switches correctly on dark mode without any per-component dark: overrides.

Duotone icons in dark mode

Duotone icons (two-color, like Phosphor’s Duotone weight) need extra care in dark mode. The secondary layer is usually a lighter or semi-transparent version of the primary — which doesn’t auto-adapt.

Duotone Color Playground

Adjust two-tone colors and opacity before applying them to your icon set.

Use CSS variables for both primary and secondary colors:

:root {
  --icon-duo-primary:   theme(colors.blue.600);
  --icon-duo-secondary: theme(colors.blue.200);
}
.dark {
  --icon-duo-primary:   theme(colors.blue.400);
  --icon-duo-secondary: theme(colors.blue.900);
}

Troubleshooting dark mode icon bugs

SymptomCauseFix
Icon invisible in dark modeHardcoded dark fill colorReplace with currentColor
Icon invisible in light modeHardcoded white fillReplace with currentColor
Icon wrong color after invertColored icon, not just blackSwitch to currentColor
Icon flickers on theme switchCSS transition on color only, not fillAdd transition-colors to SVG wrapper
Duotone secondary layer wrongSecondary color not in CSS varsAdd separate dark-mode var for secondary
Icon invisible in Windows High ContrastColor forced by browserUse forced-colors: auto media query

High Contrast Mode

Windows and some Linux setups enable Forced Colors (High Contrast) mode, which overrides CSS colors. SVGs set with currentColor adapt automatically; hardcoded colors or CSS variable fallbacks may not.

@media (forced-colors: active) {
  .icon {
    color: ButtonText;
  }
}
WCAG and forced colors

The WCAG 2.2 guidelines require icon contrast in both light and dark modes. Using currentColor and Tailwind’s dark-mode color scale (gray-300 for dark surfaces) keeps you within the 3:1 ratio required for non-text visual elements.

Practical patterns by framework

React + Tailwind (most common)

// Parent sets color; icon inherits
function NavItem({ icon: Icon, label, active }) {
  return (
    <button className={cn(
      "flex items-center gap-2 text-sm",
      active
        ? "text-blue-600 dark:text-blue-400"
        : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100"
    )}>
      <Icon size={18} />
      <span>{label}</span>
    </button>
  );
}

Next.js with system preference detection

// Tailwind handles this via class strategy in next-themes
// Just make sure your tailwind.config.ts has:
// darkMode: 'class'

Astro (static, zero JS)

<!-- Icons inherit text color set by parent; dark: via Tailwind -->
<button class="text-gray-700 dark:text-gray-300">
  <svg ...><path stroke="currentColor" .../></svg>
</button>

Checklist before shipping dark mode icons

  • All icons use currentColor or CSS variable tokens, no hardcoded colors
  • text-* and dark:text-* classes are set on the icon or its parent
  • Colored/accent icons use semantic CSS variables with dark-mode overrides
  • Duotone icons have separate primary/secondary tokens for dark mode
  • Test in Chrome, Firefox, and Safari — color rendering differs slightly
  • Test with Windows High Contrast mode enabled
  • Verify icon contrast ratio meets WCAG 3:1 in both modes
Share this post