At Subito (Italy's leading classifieds platform), we recently introduced a smart way to load and dynamically color SVG icons, one that solves a product need, not just a technical one.
You might think: "SVGs? Nothing new there." And you'd be almost right. The twist is why we built it this way.
The Problem: Decoupling Icons from the Dev Team
We were building a new feature: User Badges, visual icons displayed on the user profile and on the ad details page, to highlight seller qualities (e.g. fast responder, top seller, verified).
The requirement was clear: the product marketing and marketing teams needed to own and update these badge icons independently, without involving the dev team every time a new badge was introduced or an existing one changed.
On top of that, the icons couldn't just be static, they also needed to adapt their color dynamically.
At Subito, each category has its own brand color (e.g. motors is blue, phones is yellow), and the same badge could appear in different category contexts.
The icon had to match the surrounding palette automatically.
So the question became: how do you load an SVG dynamically and color it at runtime?
The solution: CSS mask-image
Step 1: host SVGs on S3
The first part is straightforward: upload the SVG files to an S3 bucket with public access.
This gives product teams a simple upload interface to swap icons anytime they want.
Step 2: color them dynamically with mask-image
Here's the smart bit: instead of loading the SVG as an <img> (which can't be styled) or inlining it (which couples it to the codebase), we use the CSS mask-image directive.
mask-image lets you use an image (including an SVG) as a transparency mask over an element. Only the shape of the SVG is visible, you control the fill via background-color.
Since we set that to currentColor, the icon inherits the color of its surrounding context automatically.
const maskStyle = (file: string): React.CSSProperties => ({
maskImage: `url(${ASSETS}/static/icons/badges/${file}.svg)`,
});
export const SvgIcon = ({ iconName, className }: Props) => (
<span
className={classNames(styles.maskIcon, className)}
style={maskStyle(iconName)}
aria-hidden="true"
/>
);
And the CSS module:
.maskIcon {
background-color: currentColor;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
That's it!
Change color on a parent element and the icon follows. The product team uploads a new SVG to S3, and it's live, no code changes, no deploys.
You can play with this yourself on CodePen: https://codepen.io/ale-grosselle/pen/ogzapXa
And here's the result in production:

Is this our standard for all SVGs?
Not exactly. This approach is our standard for updated icons owned and managed directly by product teams.
For everything else, we still use the classic approach: static SVG files committed to the repo, imported directly as React components.
These icons don't change often, don't need external ownership, and are better off living directly in the codebase as React components.
Bundling them avoids unnecessary HTTP requests and the latency of fetching assets from an external bucket; they're just there, instantly.
We use SVGR to automate the conversion; under the hood, it runs SVGO to optimize the SVG before generating the component, stripping unnecessary attributes and reducing file size.
A single script in package.json handles everything:
"generate:icons": "svgr --out-dir generated svg"
Drop an SVG into svg/, run the script, and a ready-to-use React component appears in generated/.
If you want to explore the code, here's the GitHub repo: https://github.com/Subito-it/articles-code/tree/main/icons
Conclusion
Our approach to loading SVGs depends on the use case:
-
Icons that need frequent updates by non-developers: S3 bucket + CSS
mask-imagefor dynamic coloring - All other icons: Static files in the repo, converted to React components with SVGR
There is rarely a single "right" solution in engineering; the best answer depends on the use case. That's exactly why at Subito we ended up with two coexisting approaches for loading SVGs: not out of inconsistency, but out of pragmatism. Each one is the right tool for its specific job.



Top comments (0)