Why SVG Symbols Ignore Your CSS (And How To Fix It)
SVG’s <symbol> and <use> elements are a powerful combination. Define a graphic once, then reuse it anywhere—across multiple SVGs or pages. It’s efficient, maintainable, and lightweight. But there’s a catch: the moment you try to animate or restyle elements inside a referenced symbol with CSS, nothing happens. Colours remain stuck, and animations won’t play.
The culprit is the Shadow DOM. When a browser renders <use>, it creates an encapsulated copy of the referenced <symbol> in a protected shadow tree. Standard CSS from outside can’t cross that boundary to reach elements inside. Even an animation that works on a regular element stops dead when applied to a <use> instance.
This behaviour is intentional, preserving consistency and predictability for reused content. But it becomes infuriating when you want two instances of the same symbol to blink at different times or display different colours without duplicating the entire graphic.
Bridging The Gap With CSS Custom Properties
Regular CSS values can’t cross the Shadow DOM barrier, but CSS Custom Properties can. While you can’t directly target elements inside a <symbol>, you can pass custom property values to them. If you set custom property values in an inline style on a <use> instance, those properties become available to the underlying symbol content.
Here’s the pattern in practice. Start by referencing a variable inside the symbol’s markup where you want dynamic behaviour:
@keyframes tapping {
0%, 60%, 100% { --foot-rotate: 0deg; }
20% { --foot-rotate: -5deg; }
40% { --foot-rotate: 2deg; }
}
use[data-outlaw="1"] {
--foot-rotate: 0deg;
animation: tapping 1s ease-in-out infinite;
}
Then, instead of trying to style the symbol’s inner elements from external CSS, attach an inline style to the <use> instance that defines that variable:
<symbol id="outlaw-1">
<g class="outlaw-1-foot" style="
transform-origin: bottom right;
transform-box: fill-box;
transform: rotate(var(--foot-rotate));">
<!-- ... -->
</g>
</symbol>
Because custom properties inherit, a browser’s cascade makes those values available to the shadowed elements. You can pass as many values as you like—fill, opacity, transform, or any other animatable property—and each <use> instance can carry its own distinct set.
Multi-Coloured Icons
A practical application is an icon system. Define a single icon symbol, then apply theme-specific colours per instance. For example, define an icon with a default fill via a variable:
<symbol id="icon-bluesky">
<path fill="var(--icon-fill, currentColor)" d="..." />
</symbol>
Then, for a header or footer where the icon needs to differ, pass new colour values inline:
<header>
<svg xmlns="http://www.w3.org/2000/svg">
<use href="#icon-bluesky" style="--icon-fill: #2d373b;" />
</svg>
</header>
<footer>
<svg xmlns="http://www.w3.org/2000/svg">
<use href="#icon-bluesky" style="--icon-fill: #590d1a;" />
</svg>
</footer>
The icons are identical shapes, but their inline styles give them unique appearances. No need to maintain multiple SVG files for different themes.
Data Visualisations That Scale
Beyond styling, this technique can channel data into SVG geometry. Imagine a profile comparing Wild West lawmen: Wyatt Earp, Pat Garrett, and Bat Masterson. By defining symbols for a career bar, an arrest badge, and a kill icon, each can accept custom properties that control its visual attributes.
--career-lengthadjusts thewidthof the career bar.--career-colourchanges thefillcolour of that bar.--arrest-scalecontrols the arrest badge size.--kill-colourdefines thefillcolour of the kill icon.
Each profile is constructed with <use> elements that share the same symbols but pass different inline variables, altering their lengths, sizes, and colours.
<svg xmlns="http://www.w3.org/2000/svg">
<g id="wyatt-earp">
<use href="#career-bar" style="--career-length: 400; --career-color: #769099;"/>
<use href="#arrests-badge" style="--arrest-scale: 2;" />
<!-- ... -->
<use href="#arrests-badge" style="--arrest-scale: 2;" />
<use href="#arrests-badge" style="--arrest-scale: 1;" />
<use href="#kills-icon" style="--kill-color: #769099;" />
</g>
<g id="pat-garrett">
<use href="#career-bar" style="--career-length: 300; --career-color: #f7bea1;"/>
<use href="#arrests-badge" style="--arrest-scale: 2;" />
<!-- ... -->
<use href="#arrests-badge" style="--arrest-scale: 2;" />
<use href="#arrests-badge" style="--arrest-scale: 1;" />
<use href="#kills-icon" style="--kill-color: #f7bea1;" />
</g>
<g id="bat-masterson">
<use href="#career-bar" style="--career-length: 200; --career-color: #c2d1d6;"/>
<use href="#arrests-badge" style="--arrest-scale: 2;" />
<!-- ... -->
<use href="#arrests-badge" style="--arrest-scale: 2;" />
<use href="#arrests-badge" style="--arrest-scale: 1;" />
<use href="#kills-icon" style="--kill-color: #c2d1d6;" />
</g>
</svg>
Because these are CSS custom properties, you can animate them too, letting differences between data points highlight themselves dynamically. Custom properties effectively bind visual attributes like colour, length, and scale to the underlying semantic data.
Ambient Character Animations
This technique shines for constructing complex characters that stay lean in markup. To build reusable figures with subtle life—blinking eyes, tapping feet, twitching moustaches—define the character once inside a <symbol>. Then, pass motion data via custom properties to each <use> instance.
Take blinking. Place an SVG group over the character’s eyes, and control its opacity with a variable:
<symbol id="outlaw-1" viewBox="0 0 712 2552">
<g class="eyelids" style="opacity: var(--eyelids-opacity, 1);">
<!-- ... -->
</g>
</symbol>
Define the blink animation by varying --eyelids-opacity:
@keyframes blink {
0%, 92% { --eyelids-opacity: 0; }
93%, 94% { --eyelids-opacity: 1; }
95%, 97% { --eyelids-opacity: 0.1; }
98%, 100% { --eyelids-opacity: 0; }
}
Apply the synchronous animation to all characters, then stagger them by giving each a different --blink-delay:
use[data-outlaw="1"] { --blink-delay: 1s; }
use[data-outlaw="2"] { --blink-delay: 2s; }
use[data-outlaw="7"] { --blink-delay: 3s; }
Similarly, a foot tap needs a rotation variable passed to its group:
<symbol id="outlaw-1" viewBox="0 0 712 2552">
<g class="outlaw-1-foot" style="
transform-origin: bottom right;
transform-box: fill-box;
transform: rotate(var(--foot-rotate));">
</g>
</symbol>
With its own animation keyframes:
@keyframes tapping {
0%, 60%, 100% { --foot-rotate: 0deg; }
20% { --foot-rotate: -5deg; }
40% { --foot-rotate: 2deg; }
}
You can combine several animations into a single declaration per character, choreographing eyes, feet, and whiskers without adding a single extra element to the SVG:
use[data-outlaw] {
--blink-duration: 4s;
--eyelids-opacity: 1;
--foot-rotate: 0deg;
--jiggle-x: 0px;
animation:
blink var(--blink-duration) infinite var(--blink-delay),
jiggle 1s ease-in-out infinite,
tapping 1s ease-in-out infinite;
}
The result is a set of characters that feel distinct and alive, all sharing one base <symbol>. Their individuality is entirely defined by the CSS custom properties attached to each instance.
Common Pitfalls
The technique is robust, but a few traps are worth avoiding:
- Variables must be referenced inside the symbol. A custom property only works if it’s used via a
var()in a property declaration within the<symbol>. Otherwise, nothing updates. - Use fallback values. Always include a fallback alongside the variable, such as
opacity: var(--eyelids-opacity, 1);, to ensure elements render correctly if a value isn’t passed. - Watch the cascade. Inline styles via the
styleattribute take precedence, and custom properties follow normal cascade rules. Mixing external CSS and inline styles can produce surprising overrides. - Use DevTools to debug. Select a
<use>instance and inspect its Computed Styles panel. It will show you which custom properties are active and help you trace inheritance issues.
The Shadow DOM barrier around <symbol> and <use> is a deliberate design choice, but it doesn’t have to be a limitation. CSS custom properties act as a sanctioned bridge across that invisible boundary. They allow you to pass colour, motion, and personality into your reused graphics, keeping assets light, consistent, and beautifully animated.



