Theming Animated SVG Icons with currentColor

Part of Animating SVG with CSS in Core CSS Animation Fundamentals.

The problem

A design system ships forty icons, a dozen of them animated: a bell that rings, a heart that fills, a chevron that rotates, a checkmark that draws. Each was exported from a design tool with colours baked in. In dark mode the dark-grey icons vanish against the background. In a destructive button the icons stay grey while the label turns red. Someone fixes that by duplicating icons per colour, and the animations now have to be maintained in three places.

Icons that animate are still icons: their colour should come from context, and their motion should be defined once.

Root cause analysis: colour flows from the text, not the file

currentColor is a keyword for the element’s computed color. When an inline SVG’s shapes use fill="currentColor" or stroke="currentColor", they take the text colour of wherever the icon is placed. A button that turns red on hover turns its icon red too; a dark theme that flips text to off-white flips icons with it. No icon-specific rule is needed, and transitions on the parent’s color carry through.

Custom properties handle the second colour. A two-tone icon can use fill: var(--icon-accent, currentColor) on its accent shape. Properties inherit into inline SVG, so a component can set --icon-accent once and every icon inside it follows.

Where the SVG lives matters. Inline svg elements and use references to symbols defined in the same document inherit colour and custom properties, and can be animated by page CSS. SVG loaded through img or as a CSS background is a separate document: currentColor inside it refers to its own default, page styles cannot reach it, and it cannot follow the theme without being re-exported.

Animations should target geometry, not colour. If the heart “fills” by animating fill from transparent to currentColor, every icon instance repaints each frame. If it fills by scaling a pre-coloured inner shape from 0 to 1, the colour is painted once and the animation is a transform — which also means a theme switch mid-animation does not restart anything.

Where an animated icon gets its coloursThe icon file carries shapes and motion; the page supplies colour.Where an animated icon gets its coloursTheme tokenslight or darkComponentcolorbutton statecurrentColorprimary shapes--icon-accentsecond toneAnimatedgroupstransform only
The icon file carries shapes and motion; the page supplies colour.

Step-by-step resolution

Converting an exported icon setOnce per icon, then never again per theme or state.Converting an exported icon set1Run the icons through an optimiser configured to replace fill and stroke colours withcurrentColor.No hex values left in shapes2Mark accent shapes with a class and fill: var(--icon-accent, currentColor).Two-tone icons without duplication3Inline the animated icons, or define symbols once and reference them with use.Same-document inheritance4Wrap animated parts in groups with transform-box: fill-box.Motion pivots on each shape5Trigger animations from the parent button's state attribute.Icons never need their own classes6Add a reduced-motion block that shows each end state.
Once per icon, then never again per theme or state.

Production code pattern

<button class="like" aria-pressed="false">
  <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
    <path class="heart-outline" fill="none" stroke="currentColor" stroke-width="2"
          d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10z"/>
    <path class="heart-fill" fill="var(--icon-accent, currentColor)"
          d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10z"/>
  </svg>
  Like
</button>
.like {
  color: var(--color-text-body);
  --icon-accent: var(--color-accent-600);
  transition: color 150ms ease-out;
}
.like[aria-pressed="true"] { color: var(--color-accent-700); }

.icon { inline-size: 1.25em; block-size: 1.25em; }

.like .heart-fill {
  transform-box: fill-box;
  transform-origin: center;
  scale: 0;                                     /* colour painted once; motion is scale */
  transition: scale 260ms cubic-bezier(0.34, 1.5, 0.64, 1);
}
.like[aria-pressed="true"] .heart-fill { scale: 1; }

@media (prefers-reduced-motion: reduce) {
  .like, .like .heart-fill { transition: none; }
}

Rendering Impact: paint for the icon region when state changes, then no per-frame colour work. The fill grows by scale; its colour comes from inherited custom properties that are resolved once, so switching theme during or after the animation does not add per-frame paint.

The example inlines the icon so selectors like .like .heart-fill reach its shapes. If the set is instead defined once as symbol elements and referenced with use, styles reach the cloned shapes through inheritance, not selectors, in most engines: the clones live in a shadow tree. Inherited properties such as color, fill and custom properties flow in; class selectors like .like .heart-fill may not match the clone. Where they do not, move the animated properties onto the symbol’s own style using custom properties — style="scale: var(--heart-scale, 0)" — and set --heart-scale from the button.

How each delivery method handles theming and animationOnly inline SVG gives full control; symbols inherit but may not match selectors.How each delivery method handles theming and animationFollows currentColorPage CSS can animate partsCustom properties inheritInline svgYesYesYesuse ofsame-documentsymbolYesVia inherited propertiesYesimg src=icon.svgNoNoNoCSS mask-imagewith iconVia background-colorWhole icon onlyNo
Only inline SVG gives full control; symbols inherit but may not match selectors.

Icon sets at scale

A page with a hundred icons, all subscribed to theme colours, pays for colour once per theme change — every icon repaints when color changes, which is a single frame of work. The cost that scales badly is per-frame colour animation. A list of fifty “like” buttons each transitioning fill on a shared hover rule repaints fifty regions per frame; the scale-based fill repaints nothing per frame after the first.

Keep animation definitions in the icon stylesheet next to the symbols, keyed on a small set of state attributes — aria-pressed, aria-expanded, data-state — so that every component using the icon gets its motion without writing any.

Verification checklist

Constraints and trade-offs

  • Shapes inside use shadow trees may not match descendant selectors; rely on inherited custom properties.
  • currentColor makes an icon follow text colour everywhere, which is wrong for brand marks that must keep their colours.
  • Icons delivered as img cannot be themed or partially animated by the page.
  • Optimisers that merge paths can combine accent and primary shapes; exclude those icons from merging.
  • Colour transitions on the parent still repaint icons during the transition; keep them short.

Frequently asked questions

Why is my SVG icon invisible in dark mode?

Its shapes use a hard-coded dark colour. Replace the fill and stroke values with currentColor so the icon follows the surrounding text colour.

Can I animate parts of an icon referenced with use?

Yes, through inherited properties. Selectors may not reach the cloned shapes, but custom properties set on the host inherit into them, so animate through variables.

Should an icon animate its fill colour?

Avoid it for icons used many times. Animate a pre-coloured shape’s scale or opacity instead, which keeps motion off the paint path.

Do icons loaded with img follow the theme?

No. They are separate documents. Inline them or use same-document symbols for themeable, animatable icons.