Skip to content
HomeBlogStyling SVG with CSS
Styling & motion

How to Style SVG with CSS: fill, currentColor and Theming

SM
SVG Maker AI
June 24, 2026
5 min read

The most-asked SVG question on Stack Overflow is how to change the colour of one, and it has fifty answers because the correct one depends on something the question never mentions: how the file got onto the page. Get that right and styling an SVG is ordinary CSS. Get it wrong and no rule you write will do anything at all.

1. Embedding decides whether any of this works

An inline SVG is part of your document. Your stylesheet sees every node in it, and everything below applies. The same file loaded through <img src="icon.svg"> or a CSS background-image is a separate document: external CSS cannot reach inside it, custom properties do not inherit into it, and currentColor has nothing to inherit from.

A file that must be recoloured by the page has to be inline, or the colour has to come from somewhere other than the file — which is section 8. If the drawing only ever needs one appearance, put the styles in a <style> block inside the file and it will work under any embedding method.

2. Presentation attributes lose every argument

fill="blue" in the markup is not the same kind of thing as fill: blue in a stylesheet. Attributes like fill, stroke and stroke-width are presentation attributes, and they sit below author styles in the cascade — beneath even a bare type selector.

cascade.html
<!-- The attribute says blue. -->
<circle cx="50" cy="50" r="40" fill="blue" />

<style>
  /* Any CSS rule beats it — even a bare type selector,
     with no id and no !important. Presentation
     attributes sit below author styles in the cascade,
     which is why they are so easy to override. */
  circle { fill: orange; }
</style>

<!-- An inline style still wins, exactly as in HTML. -->
<circle style="fill: green" fill="blue" />

This is the single most useful fact about styling SVG. It means you never need !important to recolour an icon, and it means an exported file full of hard-coded fills is not the obstacle it looks like. The one thing that still beats your stylesheet is an inline style attribute, exactly as in HTML — and those are what editors write when you set an opacity or a gradient.

3. fill and stroke, not color and background

SVG has its own vocabulary, and reaching for the HTML equivalents is why so many first attempts silently fail:

  • fill is the interior of a shape — the nearest thing to background-color, but it applies to the shape rather than a box.
  • stroke is the outline, with stroke-width, stroke-linecap and stroke-dasharray controlling how it is drawn.
  • color does almost nothing on its own. It sets no visible property directly — it exists here to give currentColor something to resolve to.
  • background does not paint an SVG shape at all. To put a colour behind the drawing, draw a rectangle.

A useful rule of thumb: icons drawn as outlines respond to stroke and ignore fill, and solid icons do the reverse. If a colour change appears to do nothing, you are usually setting the property the drawing does not use.

4. currentColor handles most theming on its own

currentColor resolves to the computed value of the color property, and color is inherited. Write it into the fills and the icon takes its colour from whatever it sits inside — no selector has to target the SVG, and hover, focus and dark mode all work with rules you already have.

current_color.html
<!-- The icon carries no colour of its own. -->
<svg viewBox="0 0 24 24" class="icon" aria-hidden="true">
  <path fill="currentColor" d="M12 2l3 7h7l-6 4 2 7-6-4z" />
</svg>

<style>
  .button        { color: #f97316; }
  .button:hover  { color: #111111; }

  /* The icon follows. currentColor resolves to the
     computed value of the color property, which is
     inherited — so no selector reaches into the SVG. */
</style>

For a single-colour icon this is the whole job, and it is why the convention is worth adopting at export time rather than patching in later. It has one real limit: there is exactly one color in scope, so a drawing that needs two colours theming independently needs the next section.

5. Custom properties, for more than one colour

CSS custom properties inherit the same way and there can be as many as you like, so a two-tone logo becomes two variables and a theme switch becomes a redefinition.

theme.css
/* Custom properties do the thing currentColor cannot:
   more than one colour in the same drawing. */
:root {
  --logo-body:   #f97316;
  --logo-accent: #facc15;
}

/* Always pass a fallback. An undefined custom property
   makes the declaration invalid at computed-value time,
   and the shape falls back to black rather than to the
   attribute you wrote in the markup. */
.logo .body   { fill: var(--logo-body,   #f97316); }
.logo .accent { fill: var(--logo-accent, #facc15); }

[data-theme="dark"] {
  --logo-body: #fff7ed;
}

The fallback in each var() is not decoration. An undefined custom property makes the whole declaration invalid at computed-value time, and the shape does not fall back to the fill you wrote in the markup — it falls back to the inherited value, which for an unstyled path means black. A variable typo therefore produces a black logo rather than the original colour, and that is a surprisingly hard thing to debug without knowing it.

6. Hover, focus, and the shape with no interior

SVG elements accept the ordinary dynamic pseudo-classes, so :hover, :focus and :focus-visible behave as expected — and transitions on fill and stroke work, which they do not in HTML, since those properties do not exist there.

hover.svg
<svg viewBox="0 0 100 100" class="card-icon"
     xmlns="http://www.w3.org/2000/svg">
  <style>
    /* Scoped inside the file, so this keeps working
       even when the file is loaded through <img>. */
    .plate {
      fill: #fff7ed;
      stroke: #f97316;
      stroke-width: 2;
      transition: fill .25s ease;
    }
    .card-icon:hover .plate { fill: #facc15; }

    /* An unfilled shape has no interior to hit, so
       hovering the middle of this ring does nothing
       until you say otherwise. */
    .ring {
      fill: none;
      stroke: #111111;
      pointer-events: all;
    }
  </style>

  <rect class="plate" x="10" y="10" width="80" height="80" rx="10" />
  <circle class="ring" cx="50" cy="50" r="25" stroke-width="2" />
</svg>

The trap in that sample is pointer-events. A shape with fill="none" has no interior to hit, so the pointer passes straight through the middle of a ring or an outlined icon and only the stroke itself responds. Setting pointer-events: all — or putting the hover on a wrapper rather than the shape — fixes a bug that otherwise looks like a broken selector.

7. Reaching into <use> content

A sprite referenced with <use> is cloned into a shadow tree, and your selectors stop at the boundary: .icon .body matches nothing. Inheritance still crosses it, though, which is exactly why the two mechanisms above are the ones worth learning.

sprite_theming.html
<!-- <use> clones the symbol into a shadow tree, and
     your selectors cannot reach inside it:

       .icon .body { fill: red; }   <- does nothing

     Two things still cross the boundary, because both
     are inherited values rather than selectors. -->

<symbol id="star" viewBox="0 0 24 24">
  <path class="body"   fill="currentColor"    d="..." />
  <path class="accent" fill="var(--accent, #facc15)" d="..." />
</symbol>

<style>
  .icon { color: #f97316; --accent: #facc15; }
</style>

<svg class="icon"><use href="#star" /></svg>

So a sprite whose shapes reference currentColor and a couple of custom properties stays themeable from the outside, while one with hard-coded fills is frozen the moment it becomes a symbol. That decision is made when the sprite is built, and it is awkward to reverse.

8. When the SVG is a background image

Sometimes inlining is not an option and the file has to stay a background. Custom properties will not reach it — but the colour does not have to come from the file at all. Use the SVG as a mask and let the page supply the paint.

mask.css
/* Does NOT work. An SVG loaded as a background is a
   separate document. It never sees the page's custom
   properties, so var(--brand) inside it is undefined. */
.icon {
  background-image: url("icon.svg");
}

/* Works. The file supplies only the shape; the colour
   comes from the page, where a variable is perfectly
   ordinary. */
.icon {
  background-color: var(--brand, #f97316);
  mask-image: url("icon.svg");
  mask-size: contain;
  mask-repeat: no-repeat;
}

The drawing becomes a stencil: its alpha channel decides which pixels show, and background-color decides the colour. That makes it themeable with any variable you like. The trade is that the whole shape becomes one colour, so it suits single-tone icons and not illustrations. The older workaround — stacking filter functions until roughly the right hue appears — is worth avoiding now that masks are widely supported.

Where to go next

One thing to watch if you style by id: our optimiser removes ids at the higher compression settings when nothing inside the file references them. It cannot see your stylesheet, so an id used only by external CSS looks unused to it. Class attributes are never touched, which makes classes the safer hook for styling — and the reason the samples above use them.

The editor's React export converts attribute names to their JSX spellings and class to className; it does not rewrite fills to currentColor or wire colours to props, so that part is yours to add — the React conversion guide covers how. For tiles that follow a theme rather than shipping twice, the pattern generator produces markup you can point at the variables above.