How to implementing dark mode for svg icons using css custom properties
- Step 1Pick which colours should react to the theme — Decide per colour: the icon's foreground usually should flip; a coloured brand accent often should not. Map only the themeable hexes — remember any explicit mapping disables auto-detect, so the rest stay fixed.
- Step 2Inject the variables — Drop the SVG into the CSS Variable Injector and map, e.g.,
#1a1a2e→--icon-fg. Or leave the map empty for a quick blanket parameterisation that auto-names everything--svg-color-N. Download the*-css-vars.svg. - Step 3Add fallbacks for safety — The output is bare
var(--icon-fg). Hand-edit tovar(--icon-fg, #1a1a2e)so icons still render their light value before any theme CSS applies — avoids a blank-icon flash on first paint. - Step 4Inline the SVG in your HTML — Paste the markup directly into the page or component template. CSS variables do not reach an SVG used as
<img src>orbackground-image, so external references won't theme. - Step 5Declare light and dark values — Author the CSS yourself (the tool won't):
:root { --icon-fg: #1a1a2e; }and@media (prefers-color-scheme: dark) { :root { --icon-fg: #f0f0f0; } }. Icons now follow the system theme automatically. - Step 6Wire up a manual or framework toggle — For user control:
[data-theme="dark"] { --icon-fg: #f0f0f0; }and setdata-themeon<html>via JS. For Tailwind class mode:.dark { --icon-fg: #f0f0f0; }. The injected SVG needs no further change.
Dark-mode wiring options (the SVG stays the same)
The injector sets up var(--icon-fg) once. These are the CSS strategies you layer on top — all author-written, since the tool emits the SVG only.
| Strategy | CSS you add | Triggered by |
|---|---|---|
| System preference | @media (prefers-color-scheme: dark) { :root { --icon-fg:#f0f0f0; } } | OS / browser dark setting |
| Manual data-attribute toggle | [data-theme="dark"] { --icon-fg:#f0f0f0; } | JS sets data-theme on <html> |
| Tailwind class strategy | .dark { --icon-fg:#f0f0f0; } | Tailwind adds .dark to <html> |
| Multiple themes | [data-theme="ocean"] { --icon-fg:#0077b6; } … | Any number of named themes |
| Per-section override | .sidebar { --icon-fg:#9aa0c0; } | Scope to an ancestor element |
What flips and what stays in a themed icon
Outcome of mapping only the foreground colour. Auto-detect is off because an explicit mapping is present.
| Colour role | Mapped? | Behaviour in dark mode |
|---|---|---|
Icon foreground #1a1a2e | Yes → --icon-fg | Flips to the dark value via CSS |
Brand accent #e02d3c | No | Stays the same in both themes |
| Gradient stop hex | Yes if you map it | Flips with the gradient if mapped |
Named currentColor | No (not detectable) | Follows text color instead — handle separately |
3-digit shorthand #03f | Browser: only as #03f | Normalise to 6-digit hex first |
Cookbook
Drop-in fragments. The injector produces the SVG; the CSS blocks are yours to add.
System-preference dark mode, no JS
Map the foreground, inline the SVG, and let a media query swap the value.
Injector mapping: #1a1a2e → --icon-fg
Inline SVG:
<svg viewBox="0 0 24 24">
<path fill="var(--icon-fg, #1a1a2e)" d="..."/>
</svg>
CSS you add:
:root { --icon-fg:#1a1a2e; }
@media (prefers-color-scheme: dark) {
:root { --icon-fg:#f0f0f0; }
}Manual toggle with data-theme
User clicks a switch; JS sets data-theme="dark" on <html>; icons respond instantly.
CSS:
:root { --icon-fg:#1a1a2e; }
[data-theme=dark]{ --icon-fg:#f0f0f0; }
JS:
document.documentElement
.setAttribute('data-theme', isDark ? 'dark' : 'light');
The same injected SVG works for both states.Tailwind class strategy
Tailwind's .dark class on <html> flips the variable. The SVG needs no Tailwind classes itself.
/* global.css */
:root { --icon-fg:#1a1a2e; }
.dark { --icon-fg:#f0f0f0; }
<html class="dark">
<svg><path fill="var(--icon-fg)" d="..."/></svg>
</html>Keep the brand accent fixed, flip only the UI colour
Map one colour; the accent stays put because adding any mapping disables auto-detect.
Injector mapping (one row): #1a1a2e → --icon-fg
Result:
<path fill="var(--icon-fg)"/> <-- flips in dark mode
<path fill="#e02d3c"/> <-- accent stays red
CSS:
:root{--icon-fg:#1a1a2e} .dark{--icon-fg:#f0f0f0}More than two themes
Beyond light/dark, add as many named themes as you like — each just redefines the variable.
:root { --icon-fg:#1a1a2e; }
.dark { --icon-fg:#f0f0f0; }
[data-theme=ocean]{ --icon-fg:#0077b6; }
[data-theme=forest]{ --icon-fg:#2d6a4f; }
One injected SVG serves every theme.Edge cases and what actually happens
Icon doesn't theme because it's an `<img>`
ExpectedThe number-one dark-mode failure: the SVG was referenced via <img src> or background-image, so the page's :root variables never reach it. Inline the SVG markup. If you must use <img>, you can't theme with variables — ship a media-query-swapped src or duplicate assets instead.
Blank icon flash before theme CSS loads
Add fallbackThe tool emits bare var(--icon-fg); if the stylesheet defining it hasn't applied yet (or is missing), the fill is empty and the icon can render invisibly. Add a fallback in the output: var(--icon-fg, #1a1a2e). The fallback is the light value, so first paint looks correct.
Accent colour flipped when it shouldn't
By designIf you used an empty map (auto-detect), every colour — including the brand accent — became a variable and may now flip in dark mode. To freeze the accent, instead map only the themeable colours explicitly; one explicit mapping turns auto-detect off so the accent stays hardcoded.
Variable defined in a CSS Module doesn't reach the icon
Scope gotchaDeclaring --icon-fg inside a component-scoped CSS Module can prevent it cascading to the inline SVG. Put theme variables in global CSS on :root (or a shared ancestor), not in scoped module styles.
currentColor would be enough for monochrome icons
Consider firstIf an icon is single-colour and should simply match its surrounding text in both themes, fill="currentColor" plus a themed text color needs no per-icon variable. This injector doesn't emit currentColor; svg-to-tailwind (default mode) does.
3-digit shorthand colours not matched in the browser tool
Match riskThe colour picker emits 6-digit hex, and the browser tool doesn't pre-expand the document, so a #03f foreground won't be hit by a #0033ff mapping. Normalise the SVG to 6-digit hex first (e.g. with svg-hex-swapper).
Hard transition with no animation
ExpectedVariables flip instantly when the theme changes. If you want a smooth fade, add a CSS transition on the property (where supported) or use a registered @property. The injector only sets up the var() reference; the transition is your CSS.
Free plan can't run the tool
402 upgrade requiredThe injector requires Pro. The dark-mode CSS techniques themselves are free and standard; only the SVG rewriting step is gated. SVG size limits (Pro 50 MB) won't be a factor for icons.
Frequently asked questions
Do I need JavaScript for dark mode SVG icons?
No. With injected var(--name) colours and a @media (prefers-color-scheme: dark) block, icons follow the system theme with zero JS. JavaScript is only needed for a user-controlled toggle, and even then it just sets a class or data-theme attribute.
Why must the SVG be inline?
CSS custom properties cascade within a single document. An inline SVG shares the page's document and inherits its variables; an SVG loaded via <img src> or background-image is isolated and never sees them. Inline the markup for theming to work.
Does the tool write the dark-mode CSS for me?
No. It outputs the rewritten SVG only. You author the :root light values and the dark overrides (@media, .dark, or [data-theme]). The cookbook gives copy-paste blocks for each strategy.
How do I keep some colours fixed across themes?
Map only the colours that should flip. Because adding any explicit mapping disables auto-detection, the colours you don't map stay hardcoded and therefore identical in light and dark mode — ideal for trademark colours.
Does it work with Tailwind's dark mode?
Yes. Tailwind's class strategy adds .dark to <html>. Add .dark { --icon-fg: #f0f0f0; } to your global CSS and the injected icons flip. The SVG itself needs no Tailwind utility classes.
Can I support more than light and dark?
Yes. Define any number of [data-theme="…"] blocks, each redefining the variables. One injected SVG serves every theme because it only references the variable names, not the values.
How do I avoid a flash of the wrong colour?
Two parts: (1) add a fallback in the SVG output, var(--icon-fg, #1a1a2e), so first paint shows the light value; (2) set the theme attribute/class as early as possible (an inline script in <head>) so the dark value applies before content renders.
Will gradients theme too?
Yes if you map the gradient's stop-color hexes. The injector rewrites any targeted hex, including inside <stop>, so a themed gradient is just multiple mapped colours.
Should I add transitions for a smooth theme change?
Optional. A CSS transition on fill/stroke (or a registered custom property) fades the change where supported. The injector doesn't add transitions; layer them in your CSS.
Does this work in React with CSS Modules?
Yes, provided the theme variables live in global CSS, not in a scoped CSS Module. Global :root (and .dark) variables cascade into inline SVGs regardless of a component's module scope.
What about icons that should just match text colour?
Use currentColor instead of a named variable for those — it inherits the element's text color and themes for free when the text colour themes. This injector doesn't emit currentColor; svg-to-tailwind in default mode produces it.
Can I batch this across a whole icon set?
Yes — run the same mapping over every icon via the API + @jadapps/runner so the entire set shares one theming interface. See the automation guide for the pipeline; then write one global CSS file with the light/dark values.
Privacy first
Every JAD SVG tool runs entirely in your browser using the DOM API and Canvas. Your SVG files never leave your device — verified by zero outbound network requests during processing.