How to change SVG colors with CSS
An SVG's color lives in one of three places: presentation attributes in the markup, a style attribute on the element, or a stylesheet. They cascade in that order of increasing specificity, and knowing which one you're fighting is most of the work.
1. Presentation attributes are the weakest
Most exported SVGs paint with a fill attribute. It behaves like an author-level default: any CSS rule that matches the element beats it, even a low-specificity type selector.
<!-- markup --> <svg viewBox="0 0 24 24"> <path fill="#2f6df6" d="M4 4h16v16H4z" /> </svg> /* wins */ svg path { fill: crimson; }
2. Inline styles do not
Illustrator and Sketch often emit style="fill:#2f6df6" instead. That's an inline style — your stylesheet loses unless you reach for !important. The better fix is to strip the inline paint from the file. The Optimizer does this; so does the Color Replacer, which rewrites every occurrence of a color wherever it's declared.
3. currentColor is the one you want
For icons, replace every hard-coded fill with the currentColor keyword. The icon then inherits the text color of its container, which means hover, focus and dark mode all work without a second rule.
<svg viewBox="0 0 24 24" fill="currentColor">…</svg> .btn { color: #18212e; } .btn:hover { color: #2f6df6; } /* icon follows */
4. Two colors, one icon
When an icon needs a second tone, keep currentColor for the primary shape and drive the accent from a custom property with a fallback.
<path fill="currentColor" d="…" /> <path fill="var(--icon-accent, #2f6df6)" d="…" />
One caveat, and it is the one that costs the most time: custom properties only resolve when the SVG is inline in the document. That restriction is not specific to custom properties — it is the next section.
5. How you embed the SVG decides what CSS can do
Before debugging a selector that refuses to apply, check how the file got onto the page. Four of the five ways to embed an SVG put it in a separate document, and a separate document inherits nothing from yours — not your stylesheet, not currentColor, not custom properties.
- Inline
<svg>— part of your DOM. Everything works: stylesheets,currentColor, custom properties, hover states, JavaScript. <img src="icon.svg">— separate document. No CSS reaches in. You can still size and rotate the element, andfilterapplies to the rendered result, but you cannot change a fill.background-image: url(icon.svg)— same restriction. This is why recoloured background icons are usually shipped as one file per colour, or encoded as a data URI with the colour baked in.<object>/<iframe>— separate document, but one you can reach with a<style>block inside the SVG file itself.<use href="sprite.svg#icon">— the awkward middle case. The instance lives in your document, so inherited properties likefillandcolorcross the boundary, but selectors do not match inside the shadow content.
If you need CSS control, inline the SVG. That is the entire rule, and it explains most "my CSS does nothing" reports.
6. Fill and stroke are separate axes
Icon sets split into two families, and applying the wrong one produces an icon that looks broken rather than miscoloured. Solid icons paint with fill and usually set no stroke. Outline icons do the opposite: fill="none" with the shape carried entirely by stroke.
/* solid set */ .icon { fill: currentColor; } /* outline set — filling it turns the icon into a blob */ .icon { fill: none; stroke: currentColor; }
Setting fill: currentColor globally on an outline set fills every enclosed area solid — the classic "my icons turned into black squares" bug. If you mix both families, scope the rule rather than applying one to all of them.
Stroke width has its own trap: strokes scale with the drawing, so an icon designed at 24 units with stroke-width="2" renders a 6px stroke at 72px. vector-effect="non-scaling-stroke" holds the stroke at a constant device width if that is what you want.
7. Gradients and patterns ignore all of this
A shape painted with fill="url(#grad)" takes its colour from the gradient definition, not from the element. No amount of CSS on the path will change it, because the path's fill is already set — to a reference.
The colours live on the <stop> elements, and those accept the same keywords everything else does:
<linearGradient id="grad"> <stop offset="0" stop-color="currentColor" /> <stop offset="1" stop-color="var(--accent, #2f6df6)" /> </linearGradient>
Both only work with inline SVG, per the previous section. If the icon has to survive being loaded through <img>, a gradient is a commitment — you will be shipping one file per colourway. Converting the gradient to a flat currentColor fill in the Color Replacer is usually the cheaper answer for interface icons.
8. Converting an existing set
Putting the above in order, for a folder of icons that currently paint themselves in whatever colour the designer picked:
- Decide how they will be embedded. If they need to respond to theme or state, they have to be inline — as a component, or through a sprite. Everything below assumes that.
- Strip inline paint first. A
style="fill:#333"left on a path will beat every rule you write afterwards. This is what makes the change stick. - Replace the remaining fills with
currentColor. One pass over the set; the Color Replacer converts every occurrence including the ones inside<style>blocks. - Check whether the set is fill-based or stroke-based and apply the matching rule from section 6. Mixing them under one selector is the fastest way to a page of black squares.
- Set colour on the container, never on the icon. The point of
currentColoris that hover, focus, disabled and dark mode are already handled by whatever rules colour the text. - Look at it in dark mode before you call it done. Icons that were invisible as near-black on white are the usual survivors of this process, and they only show up when the background flips.
The end state is an icon with no colour information in it at all — which sounds like a loss and is the entire objective. Colour becomes something the page decides, once, in CSS.