Inline SVG vs img vs CSS background
A side-by-side comparison of inline SVG, img tags and CSS backgrounds for icons, covering caching, styling, scripts, accessibility and performance.
5 min read
There are three common ways to put an SVG icon on a web page: paste the markup inline, reference the file with <img>, or attach it in CSS as a background or mask. All three display the same picture. They differ in what you can do with it afterwards, what it costs when you have hundreds of them, and how safe it is with files you did not draw.
Here is the comparison first, then what each row means in practice, then a recommendation per use case. If you want the basic syntax for each method, three ways to put an SVG icon in HTML covers that.
The comparison
Inline <svg> |
<img src="icon.svg"> |
CSS background-image / mask-image |
|
|---|---|---|---|
| Caching | Repeated in every HTML response, or cached as part of a JS bundle | Each file cached by URL and shared across pages | Cached with the stylesheet (data URI) or per file (URL) |
| Extra HTTP requests | None | One per distinct file | One per distinct file, none for data URIs |
| Style shapes with page CSS | Yes, every fill and stroke | No | No |
| Recolor a single-color icon | currentColor or CSS fill |
Only crude CSS filters | Yes, with mask-image plus background-color |
| Scripts inside the file | Can run as part of your page | Never run | Never run |
| Accessible name | role="img" with aria-label, or aria-hidden |
alt attribute |
None; the element needs its own text |
| DOM cost per icon | One node per shape, plus the root | One element | No extra element |
| Native lazy loading | No | loading="lazy" |
No |
| ID collisions between icons | Possible | Impossible | Impossible |
| Right-click, save, drag | No | Yes | No |
Caching and requests
An <img> pointing to /icons/eth.svg is downloaded once and reused from the cache on every page that shows it. With good cache headers, a returning visitor downloads nothing at all.
Inline SVG has no URL of its own. Its markup travels inside the HTML of every page, or inside a JavaScript bundle if your icons are components. For a dozen small UI icons this is a non-issue, and saving the request can even help the first render. For a few hundred multi-color logos that are each 2 to 5 KB, it adds hundreds of kilobytes to the page.
CSS backgrounds sit in between. A data URI is cached with the stylesheet, but it also makes the stylesheet bigger, and stylesheets block rendering. A URL in CSS behaves like an <img> for caching.
Styling and recoloring
This is the reason inline SVG exists in most codebases. Because the shapes are in your DOM, CSS can reach them:
<button class="icon-button" aria-label="Copy address">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
<path d="M8 8h11v11H8zM5 5h11v2H7v9H5z" fill="currentColor"/>
</svg>
</button>
The icon takes the button's text color, changes on hover and follows dark mode without a second file.
An <img> is a sealed box. You can size it, fade it with opacity, or push it through filter, but you cannot change its fill. For multi-color brand logos, that is actually what you want: they should look the same everywhere.
For single-color icons you want to tint without inlining, CSS masks are the useful middle ground. The SVG file defines the shape, and the element's background color fills it:
.mono-icon {
width: 24px;
height: 24px;
background-color: currentColor;
-webkit-mask: url(/icons/eth-mono.svg) center / contain no-repeat;
mask: url(/icons/eth-mono.svg) center / contain no-repeat;
}
The file stays cacheable and cannot run scripts, and the color comes from your CSS. The details, including browser prefixes, are in recoloring a single-color SVG with CSS mask.
Scripts and trust
Inline SVG is part of your page, so anything executable inside it (event handler attributes, javascript: links) runs with your page's privileges. That is fine for icons in your own source code. It is not fine for markup fetched from a token list or uploaded by a user.
Both <img> and CSS treat SVG as a static picture: no scripts, no interaction, no external resources. If a logo comes from anywhere you do not control, use one of those two.
Accessibility
Each method names icons differently. In order below: an image with alt, a meaningful inline SVG with a role and label, and a decorative inline SVG hidden from assistive technology.
<img src="/icons/usdc.svg" width="24" height="24" alt="USDC">
<svg role="img" aria-label="USDC" viewBox="0 0 24 24">...</svg>
<svg aria-hidden="true" viewBox="0 0 24 24">...</svg>
CSS backgrounds and masks are invisible to screen readers. That is correct for purely decorative icons, but if the icon carries meaning (an icon-only "copy" button, a logo with no text next to it), the element itself needs an accessible name, such as an aria-label or visually hidden text.
When an icon sits next to its visible label, as in a token row showing the logo and "USDC", the icon is decorative and an empty alt="" or aria-hidden="true" avoids reading the name twice.
Performance with many icons
This is where the methods diverge most. Inline SVG puts every shape into the DOM. A logo with five shapes is six elements; a grid of 1,000 logos is around 6,000 elements that the browser must style, lay out and keep in memory, before you count the grid cells themselves. Scrolling and filtering slow down accordingly.
An <img> is a single element no matter how complex the drawing. With loading="lazy", images far below the viewport are not fetched until the user scrolls near them, and decoding="async" lets the browser decode them without blocking the main thread:
<img src="/icons/sol.svg" width="32" height="32" alt="" loading="lazy" decoding="async">
Always give images width and height so the page reserves their space and does not jump while they load.
CSS backgrounds add no elements, but there is no native lazy loading, and data URIs move the cost into the stylesheet. For very large grids, the bigger win is usually rendering fewer cells at once; rendering thousands of icons in one page covers paging, virtualization and content-visibility.
Which to use
| Use case | Recommended | Why |
|---|---|---|
| Multi-color token, chain, wallet or exchange logos in lists and tables | <img> |
Cached, cheap in the DOM, lazy-loadable, safe |
| UI icons that follow text color (buttons, menus, tabs) | Inline SVG with currentColor |
Full CSS control, no requests |
| Single-color logos recolored by theme | CSS mask-image |
Recolorable, cacheable, safe |
| Logos from token lists or user uploads | <img> only |
Scripts never run |
| Icons with animated parts | Inline SVG | CSS can target each shape |
| Decorative patterns and background flourishes | CSS background-image |
No DOM cost, no semantics needed |
Mixing methods in one interface is normal. A wallet might use inline SVG for its own interface icons, <img> for every token logo in the balance list, and a mask for the mono network badge in the header. Pick per job, not per project.
One extra caution if you do inline logos with gradients: two icons using the same internal ID will paint with each other's gradient. SVG ID collisions explains why and how to prevent it.