I built a dashboard for a client last month that needed seven character avatars. To keep the page light, I defined each avatar once with <symbol> and dropped in a <use> wherever it appeared. That held up fine until the client asked for a different animation per character: separate blinking patterns, different foot-tapping speeds. My markup had nowhere to hang any of that. SVG CSS Custom Properties turned out to be the way through.
The obstacle is the Shadow DOM. When you reference a symbol with a <use> tag, the browser makes a protected copy of that element. My first instinct, the obvious but wrong move, was to put a class on the <use> tag and target the paths inside it. Nothing happened. The cascade stops dead at that boundary. For a few minutes I seriously considered inlining the whole SVG seven times, which would have wrecked the performance I was trying to protect, until I remembered that variables behave differently.
Bridging the Shadow DOM with SVG CSS custom properties
Custom properties are the exception: they penetrate the Shadow DOM. You still cannot reach in and style an element directly, but you can pass values through variables that the elements inside are already reading. I leaned on the same idea for sizing in an earlier post on creating perfect adaptive SVGs with symbols.
Setup starts in the symbol library. On the element you want to animate, write the property you care about as a variable inside its inline style. Here that element is a foot, and the property is rotation.
<!-- The bbioon Symbols Library -->
<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
<symbol id="bbioon-outlaw-1" viewBox="0 0 712 2552">
<g class="bbioon-foot" style="transform: rotate(var(--bbioon-foot-rotate, 0deg)); transform-origin: center;">
<!-- SVG paths here -->
</g>
</symbol>
</svg>
The foot now sits behind the Shadow DOM barrier, watching --bbioon-foot-rotate for a change it can act on. So in your main stylesheet you animate the variable itself, scoped to a single <use> instance.
@keyframes bbioon-tapping {
0%, 100% { --bbioon-foot-rotate: 0deg; }
50% { --bbioon-foot-rotate: -5deg; }
}
use[data-character="1"] {
animation: bbioon-tapping 0.8s ease-in-out infinite;
}
use[data-character="2"] {
animation: bbioon-tapping 1.2s ease-in-out infinite;
}
Making reusable elements feel unique
You can pass more than one value at a time, which is where SVG CSS Custom Properties start earning their keep. On that client dashboard I did not stop at tapping feet. One variable drove eyelid opacity for blinking, another jiggled facial hair. The markup stayed small and the animation still looked natural.
Multi-colored icon systems work the same way, and this is where most people first see the point. Rather than shipping ten versions of a social media icon, define one symbol and let a variable carry the fill. Each <use> tag then sets its own color through an inline style or a scoped rule. That is how the Magnificent 7 graphics stay so light at any screen size.
Pitfalls to watch for
- Missing fallbacks. Write
var(--name, default)rather than a barevar(). With no value defined, your SVG can render wrong or disappear outright. - Inheritance. These properties follow the normal cascade, so a variable set on a parent container leaks into the symbol unless something overrides it.
- Debugging. The inner paths of a symbol are awkward to inspect, but the computed variables do show up on the
<use>tag. Check there first, it is faster than guessing.
What this buys you
SVG symbols do not get used nearly as much as they should, given what they do for performance. The Shadow DOM is the annoying part, and SVG CSS Custom Properties are how you work around it: one definition reused everywhere, still animated one instance at a time.
This drops into browser rendering details fast, I know. If you have a messy SVG setup, or animations that are dragging your site down, it is probably something I have already run into. Get in touch and we can sort it out.