How to css custom properties vs. hardcoded colors in svg: a practical guide
- Step 1Identify the delivery method — Inline SVG in HTML → CSS variables resolve.
<img src>orbackground-image→ they do not. This single fact decides whether variables are even an option for that asset. - Step 2Assess the theming requirement — No runtime theme switching needed → hardcoded hex is simpler and safer. Dark mode or user-selectable themes on inline icons → custom properties are the right architecture.
- Step 3Decide per colour, not per icon — A logo's trademark gold may need to stay fixed while the UI navy should follow the theme. Map only the themeable colour in the injector; the rest stays hardcoded because adding any explicit mapping disables auto-detect.
- Step 4Consider `currentColor` for single-colour icons — If an icon should simply match its surrounding text colour,
fill="currentColor"is lighter than a named variable. The injector does not producecurrentColor— use svg-to-tailwind (default mode) for that — but it's the right comparison to make before adding a variable. - Step 5Run the injector on the colours you chose to parameterise — Map each themeable hex to a name (
#003082→--brand-primary). Leave the map empty only for a quick blanket parameterisation that auto-names everything--svg-color-N. - Step 6Author the declarations and add fallbacks — Write
:root { --brand-primary: #003082; }yourself, and addvar(--brand-primary, #003082)fallbacks where an undefined variable would render a blank shape.
Decision matrix: hardcoded vs CSS variables vs currentColor
The three colour strategies for inline SVG, scored against common needs. currentColor is a CSS keyword, not something this injector emits — it's included for an honest comparison.
| Need | Hardcoded hex | CSS variable | currentColor |
|---|---|---|---|
Works as <img src> / background | Yes | No | No |
Re-themes from one :root change | No | Yes | Only via text color |
| Multiple independent colours themeable | No | Yes (one var each) | No (one colour only) |
| Zero extra CSS to maintain | Yes | No (you write :root) | Minimal (inherits color) |
| Produced by this injector | (leave unmapped) | Yes | No — use svg-to-tailwind |
Which colour forms the injector can parameterise
The injector targets #hex strings literally. This is the practical boundary of the variables-vs-hardcoded choice.
| Colour in source | Can become a variable? | What to do otherwise |
|---|---|---|
6-digit hex #003082 | Yes — the ideal case | — |
3-digit hex #03f | Browser: only as #03f; engine expands first | Normalise to 6-digit hex first |
Named red / currentColor | No | Swap to hex, or keep currentColor deliberately |
rgb() / hsl() | No | Normalise to hex upstream |
Gradient stop-color hex | Yes | Parameterised like any other hex |
Cookbook
Side-by-side fragments showing each strategy. The :root blocks are CSS you write; the SVG fragments are what the injector produces.
Hardcoded: portable but frozen
No CSS plumbing, renders anywhere — but the only way to change the colour is to re-export or re-run a tool.
<svg viewBox="0 0 24 24"> <path fill="#003082" d="..."/> </svg> + Works as <img>, background-image, inline — everywhere. - Re-theming means editing the file again.
CSS variable: themeable but inline-only
Injector output plus the declaration you author. One :root line now controls the colour across the page.
Injector mapping: #003082 → --brand-primary
<svg viewBox="0 0 24 24">
<path fill="var(--brand-primary)" d="..."/>
</svg>
CSS you add:
:root { --brand-primary:#003082; }
.theme-night :root { --brand-primary:#9db4ff; }currentColor: the lightweight single-colour option
For a monochrome icon that should just follow its text colour. This injector does not emit currentColor; the comparison shows when a variable is overkill.
<button style="color:#003082"> <svg><path fill="currentColor" d="..."/></svg> Save </button> The icon turns navy because it inherits the button's color. No named variable needed for one-colour icons. Get this form from svg-to-tailwind (default mode).
Mixed: freeze the trademark, theme the UI colour
Map only the colour that should move with the theme. Because one explicit mapping disables auto-detect, the trademark colour is left exactly as-is.
Injector mapping (one row): #1a1a2e → --ui-fg Before: <path fill="#1a1a2e"/> (UI — themeable) <path fill="#e8b923"/> (gold trademark — fixed) After: <path fill="var(--ui-fg)"/> <path fill="#e8b923"/> <-- untouched
Auto-name vs hand-name
Empty map → generic, fast names. Explicit rows → maintainable token names. The output SVG is identical in structure; only the variable identifiers differ.
Empty map (auto): fill="var(--svg-color-1)" /* #1a1a2e */ fill="var(--svg-color-2)" /* #16213e */ Explicit rows: fill="var(--surface)" /* #1a1a2e */ fill="var(--surface-elevated)" /* #16213e */ Prefer explicit names for anything you'll maintain.
Edge cases and what actually happens
Variables silently fail when the SVG is not inline
ExpectedThe single biggest reason teams regret choosing variables: the icon was switched to an <img> or a CSS background-image later, and every var(--…) stopped resolving. If an asset might ever be referenced externally, hardcoded hex (or a re-render pipeline) is the safer default.
You adopt variables but forget you own the declarations
By designThe injector emits the SVG only. Choosing variables means committing a :root block to your stylesheet and keeping it in sync. If nobody owns that CSS, the icons render blank or fall back. Hardcoded colours have no such ongoing cost.
currentColor would have been simpler
Consider firstFor a one-colour icon that should match adjacent text, currentColor needs no named variable and no :root entry. Reaching for a custom property here adds maintenance for no benefit. Compare both before injecting.
Presentation attribute vs CSS specificity confusion
Cascade gotchaA fill="var(--x)" presentation attribute is overridable by a CSS rule targeting the same element. If you set the variable but a later CSS fill: rule wins, the colour won't change as expected. Keep colour authority in one place — either the attribute or the stylesheet, not both.
Mixing variables and hardcoded in one icon
SupportedFully supported and often correct: map the themeable colours, leave the fixed ones. Just remember the mechanic — adding any explicit mapping turns off auto-detect, so list every colour you want parameterised; the rest stay hardcoded automatically.
Variable defined in a scoped CSS Module never reaches the SVG
Scope gotchaIf --brand-primary is declared inside a component-scoped CSS Module, it may not cascade to an inline SVG outside that scope. Theming variables for SVG belong in a global stylesheet on :root (or a shared ancestor), not in scoped module CSS.
Named or rgb() colours can't participate
Not detectedIf your source uses red or rgb(0,48,130), the injector won't parameterise them — the picker emits hex and auto-detect scans only #hex. The variables-vs-hardcoded decision is moot until those are normalised to hex with svg-hex-swapper.
Gradient stops become themeable too
SupportedBecause stop-color carries a hex, mapping that hex makes the gradient stop a var(--name). This is a genuine advantage of variables over hardcoded for multi-stop gradients you want to re-theme — but only inline, like everything else.
Frequently asked questions
When should I keep colours hardcoded?
When the SVG ships as an <img>, a background image, or anywhere it isn't inlined; when there's no runtime theming requirement; or when the colour must never change (a trademark). Hardcoded hex renders universally with zero CSS to maintain.
When are CSS variables worth it?
When the SVG is inlined and needs to re-theme at runtime — dark mode, user-selectable themes, or design-token-driven recolouring. One :root change then updates every matching shape across the page.
Can I use variables for some colours and hardcoded for others?
Yes — that's the recommended pattern for icons with a fixed brand colour plus themeable UI colours. In the injector, map only the colours you want as variables; because adding any explicit mapping disables auto-detect, the unmapped colours stay hardcoded.
What is currentColor and when is it better?
currentColor is a CSS keyword that resolves to the element's inherited text color. For a single-colour icon that should match adjacent text, it's lighter than a named variable — no :root entry needed. This injector doesn't emit it; svg-to-tailwind in its default mode does.
What happens if a CSS variable isn't defined?
Without a fallback, the property has no value and the shape may render with no fill (often invisible). The injector outputs bare var(--name), so add a fallback by hand where it matters: var(--name, #003082).
Do variables work in SVG gradient stop colours?
Yes, in inline SVG. stop-color: var(--gradient-start) follows the same cascade as fill. The injector will turn a stop-color hex into a variable automatically when you map that hex.
Do CSS variables in SVG support animation?
Custom-property values can be transitioned/animated where the browser supports it (e.g. registered properties via @property in Chromium), and JavaScript can update the variable to drive smooth colour changes. The injector only sets up the var(--name) references; the animation is ordinary CSS/JS on your side.
Will variables bloat my SVG?
Marginally — var(--brand-primary) is longer than #003082. For most icons the difference is negligible. If size matters, run svg-pro-minifier after injecting; it won't strip the variable references.
Is there a performance cost to many variables?
Negligible for normal icon counts. CSS variable resolution is cheap; the cost is conceptual (more tokens to track). The bigger risk is maintenance drift between your :root block and the variables your SVGs actually reference.
Can I convert variables back to hardcoded later?
Not with this tool directly — it goes hex → variable, not the reverse. To bake a theme back into fixed colours, replace each var(--name) with its hex using a find/replace step, or re-export from your design source.
Does the choice differ for React / Vue components?
The component is inline SVG, so variables work — provided the :root (or ancestor) declarations live in global CSS, not in a scoped CSS Module. If you've already wrapped the SVG as a component, svg-to-jsx or svg-to-vue-svelte pair naturally with variable-based theming.
What's the safest default for a brand-new icon set?
If they'll be inlined and you want one theming surface, parameterise the UI colours and keep trademark colours fixed. If delivery is uncertain (some as <img>), keep hex and build a re-render step instead. Decide by delivery first, theming second.
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.