Back to Blog
S

Simple Icons CDN Theme Color Not Working? Fixing Icon Colors with Custom Dark Mode

April 23, 2026·8 min read
Simple IconsDark ModeReactCSS

Table of Contents

  • The Problem
  • Investigation
  • Step 1: Inspect the CDN Response
  • Step 2: Compare with the Site's Theme Implementation
  • Step 3: Identify the Conflict
  • The Solution
  • Core Idea
  • Implementing getThemedIconUrl
  • URL Transformation Examples
  • Usage in Components
  • Caching Analysis
  • Fade Animation
  • Alternative Approaches Considered
  • Summary

Table of Contents

  • The Problem
  • Investigation
  • Step 1: Inspect the CDN Response
  • Step 2: Compare with the Site's Theme Implementation
  • Step 3: Identify the Conflict
  • The Solution
  • Core Idea
  • Implementing getThemedIconUrl
  • URL Transformation Examples
  • Usage in Components
  • Caching Analysis
  • Fade Animation
  • Alternative Approaches Considered
  • Summary

The Problem

My site uses Simple Icons CDN to render skill icons. Following the official docs, I used the dual-color URL format for dark mode support:

<img src="https://cdn.simpleicons.org/nextdotjs/000000/ffffff" />

Expected behavior: black icons in light mode (#000000), white icons in dark mode (#ffffff).

Dark mode worked fine. But when I manually switched the site back to light mode, the icons stayed white—nearly invisible on a light background.

Investigation

Step 1: Inspect the CDN Response

Using curl to check what the CDN actually returns:

curl -s "https://cdn.simpleicons.org/nextdotjs/000000/ffffff"

The SVG content:

<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <title>Next.js</title>
  <style>path{fill:#000000} @media (prefers-color-scheme:dark){path{fill:#ffffff}}</style>
  <path d="M18.665 21.978C16.758..." />
</svg>

Key finding: the CDN's dark mode support uses @media (prefers-color-scheme: dark).

Step 2: Compare with the Site's Theme Implementation

My site uses a custom dark mode implementation:

  • User preference stored in localStorage
  • Theme toggled via document.documentElement.classList.add('dark')
  • CSS variables switched through the .dark selector

SVGs loaded via <img> tags are isolated documents—they can't see the host page's CSS classes or DOM state.

Step 3: Identify the Conflict

The problem occurs when the OS color scheme and the site theme disagree:

OS ThemeSite ThemeSVG PerceivesActual FillResult
DarkLightprefers-color-scheme: dark#ffffff❌ White icon on light bg
DarkDarkprefers-color-scheme: dark#ffffff✅ Correct
LightLightprefers-color-scheme: light#000000✅ Correct
LightDarkprefers-color-scheme: light#000000❌ Black icon on dark bg

Root cause: SVGs loaded as <img> are isolated documents that only sense the OS-level prefers-color-scheme, not the site's custom theme.

The Solution

Core Idea

Since the CDN's @media (prefers-color-scheme) mechanism is incompatible with custom themes, bypass it entirely—split the dual-color URL into single-color URLs and let the site code choose dynamically based on the current theme.

Implementing getThemedIconUrl

/**
 * Transforms Simple Icons CDN URL based on site theme.
 * CDN's dark mode uses @media (prefers-color-scheme) which
 * cannot detect custom site themes. This function splits
 * dual-color URLs into single-color URLs based on current theme.
 *
 * @param url - Original icon URL
 * @param theme - Current site theme
 * @returns Theme-appropriate single-color URL
 */
function getThemedIconUrl(url: string, theme: 'light' | 'dark'): string {
  if (!url.includes('cdn.simpleicons.org')) return url;

  try {
    const urlObj = new URL(url);
    const parts = urlObj.pathname.split('/').filter(Boolean);

    if (parts.length < 3) return url;

    const slug = parts[0];
    const lightColor = parts[1];
    const darkColor = parts[2];
    const color = theme === 'dark' ? darkColor : lightColor;

    urlObj.pathname = color === '_' ? `/${slug}` : `/${slug}/${color}`;
    return urlObj.toString();
  } catch {
    return url;
  }
}

URL Transformation Examples

Original URLLight ModeDark Mode
/nextdotjs/000000/ffffff/nextdotjs/000000/nextdotjs/ffffff
/github/_/ffffff/github (default brand color)/github/ffffff
/react/61dafb/ffffff/react/61dafb/react/ffffff

The _ placeholder is Simple Icons CDN's convention for "use default brand color."

Usage in Components

function SkillCard({ skill, colorKey, index }: SkillCardProps) {
  const { theme } = useTheme();

  const hasCustomIcon = skill.icon && /^https?:\/\//.test(skill.icon);
  const iconSrc = hasCustomIcon && skill.icon
    ? getThemedIconUrl(skill.icon, theme)
    : undefined;

  return (
    <div className="...">
      {iconSrc ? (
        <img
          key={iconSrc}
          src={iconSrc}
          alt={skill.name}
          className="h-4 w-4 object-contain animate-icon-theme-fade"
        />
      ) : (
        skill.name.charAt(0).toUpperCase()
      )}
    </div>
  );
}

Note the key={iconSrc} prop—when the theme changes and the URL changes, React unmounts the old element and mounts a new one, triggering the fade-in animation.

Caching Analysis

You might worry: does every theme switch trigger a new CDN request? No. Since light and dark modes use different URLs, the browser maintains independent cache entries for each.

Verifying the CDN's cache headers:

curl -sI "https://cdn.simpleicons.org/nextdotjs/000000" | grep -i cache
Cache-Control: public, max-age=86400, s-maxage=31536000, stale-while-revalidate=604800
cf-cache-status: HIT
Cache ParameterValueMeaning
max-age86400Browser caches locally for 24 hours
s-maxage31536000CDN edge nodes cache for 1 year
stale-while-revalidate604800Serve stale content for 7 days while revalidating

In practice: both color variants are fetched once on first load, then all subsequent switches hit the browser cache—zero latency, zero network overhead.

Fade Animation

The instant color switch on theme change felt jarring, so I added a 200ms fade-in animation for a smoother transition.

CSS definition:

@keyframes icon-theme-fade {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}

Registered in Tailwind v4's @theme inline:

@theme inline {
  --animate-icon-theme-fade: icon-theme-fade 0.2s ease-in-out;
}

Use the animate-icon-theme-fade class in components. Combined with key={iconSrc}, every theme switch replays the animation.

Alternative Approaches Considered

ApproachProsCons
Dynamic URL (chosen)Full control, cache-friendly, any colorRequires component changes
CSS dark:invertZero code change, one CSS classOnly works for B/W icons; color icons get inverted
CSS color-scheme propertyKeeps CDN dual-color URLInconsistent browser support; unreliable inside <img> SVGs
Inline SVG + currentColorMost flexibleRequires local SVG storage; loses CDN convenience

Summary

When your site uses a custom dark mode implementation (not prefers-color-scheme), Simple Icons CDN's dual-color URLs break due to SVG's isolated document behavior. The fix is to split dual-color URLs into single-color URLs and dynamically select based on the current theme, combined with the key prop to trigger React remounting and fade animations for a smooth theme-switching experience.

Key takeaways:

  1. SVGs inside <img> tags are isolated documents—they can't sense the host page's CSS classes
  2. Simple Icons CDN's dark mode relies on prefers-color-scheme, which is incompatible with custom theme systems
  3. Splitting into single-color URLs with dynamic selection is reliable and cache-friendly
  4. The key prop is the key technique for triggering React remount animations