ICON OOP
Icons & Logos
Guide

How to use SVG icons in HTML

There are four ways to get an SVG icon onto a page. They look interchangeable and they are not: each one trades caching against styling control, and picking the wrong one is how projects end up with icons they cannot theme.

HomeGuidesUsing SVG icons

Every method below renders the same icon. What differs is whether your CSS can reach inside it, whether the browser can cache it, and how many network requests you end up making. Those three things are the whole decision.

The short answer

Under about 20 icons on a page: inline SVG. More than that, or the same icons repeated across many pages: SVG sprite. A logo or illustration that never changes colour: img tag. Purely decorative and never dynamic: CSS background.

Method 1: inline SVG

Paste the SVG markup straight into your HTML.

<button class="icon-btn" aria-label="Search">
  <svg viewBox="0 0 24 24" width="20" height="20" fill="none"
       stroke="currentColor" stroke-width="2" aria-hidden="true">
    <circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/>
  </svg>
</button>

Gives you: full CSS control, including currentColor, hover states, dark mode and animation. Zero extra network requests. Every element is addressable and scriptable.

Costs you: markup weight in the HTML, which is not cached separately, and duplication if the same icon appears many times. Templating or components solve the duplication in practice.

Use it when icons need to respond to state or theme, which is most interface icons, and when the count per page is modest.

Method 2: the img tag

<img src="/icons/search.svg" width="20" height="20" alt="Search">

Gives you: simplicity, browser caching across pages, and clean HTML. Lazy loading with loading="lazy" works normally.

Costs you: all styling control. Your CSS cannot reach inside the file, so currentColor is inert and the icon cannot be recoloured, themed or animated from the page. One HTTP request per icon, though HTTP/2 makes that far less costly than it used to be.

Use it when the icon is a fixed graphic: a logo, an illustration, a full-colour brand mark. Not for interface icons that need states.

Method 3: SVG sprite with use

All your icons live in one file as <symbol> elements, and each usage references one by ID.

<!-- icons.svg -->
<svg xmlns="http://www.w3.org/2000/svg">
  <symbol id="search" viewBox="0 0 24 24" fill="none"
          stroke="currentColor" stroke-width="2">
    <circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/>
  </symbol>
</svg>

<!-- in your page -->
<svg width="20" height="20" aria-hidden="true">
  <use href="/icons.svg#search"></use>
</svg>

Gives you: one cached request for every icon on the site, small page markup, and currentColor still works so styling is preserved. It is the only method that gets both caching and colour control.

Costs you: a build step to generate the sprite, and a small caveat that referencing a sprite on a different origin is blocked by CORS. Keep the sprite on your own domain.

Use it when you have a sizeable icon set used across many pages. Full walkthrough in SVG sprites.

Method 4: CSS background or mask

.icon-search {
  width: 20px; height: 20px;
  background: url("/icons/search.svg") center / contain no-repeat;
}

/* Recolourable variant, using mask */
.icon-mask {
  width: 20px; height: 20px;
  background-color: currentColor;
  -webkit-mask: url("/icons/search.svg") center / contain no-repeat;
          mask: url("/icons/search.svg") center / contain no-repeat;
}

Gives you: icons entirely in the stylesheet with no markup at all, useful for pseudo-element decorations. The mask variant restores colour control for single-colour icons.

Costs you: the icon is invisible to assistive technology, which is correct for decoration and wrong for anything meaningful. Data-URI encoding bloats the stylesheet if overused.

Use it when the icon is genuinely decorative: a chevron on a list item, a bullet marker, a background flourish.

Side by side

InlineimgSpriteCSS bg
CSS can recolour itYesNoYesMask only
Animatable from the pageYesNoPartlyNo
Cached separatelyNoYesYesYes
Requests01 per icon1 total1 per icon
Adds page weightYesNoMinimalNo
Screen reader accessibleYesYes, via altYesNo
Needs a build stepNoNoUsuallyNo

Accessibility, whichever you choose

The rule does not change with the method: an icon either conveys information or it does not.

  • Icon-only button. The button needs an accessible name, the icon should be hidden. Put aria-label on the button and aria-hidden="true" on the SVG.
  • Icon beside visible text. The text already carries the meaning. Hide the icon with aria-hidden="true", or it gets announced twice.
  • Icon carrying meaning alone, such as a status indicator. Give it a text equivalent with role="img" and an aria-label, or visually hidden text.
  • img tag. Meaningful icons get real alt text, decorative ones get alt="". Never omit the attribute; a missing alt makes screen readers read the filename.

Full patterns in accessible icons.

In React, Vue and Svelte

Component frameworks make inline SVG practical at scale, because the duplication argument disappears: the icon is written once as a component and used everywhere.

export function SearchIcon({ size = 20, ...props }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
         stroke="currentColor" strokeWidth="2" aria-hidden="true" {...props}>
      <circle cx="11" cy="11" r="7" />
      <path d="M21 21l-4.3-4.3" />
    </svg>
  );
}

Or install the official package for your set, lucide-react, @tabler/icons-react, @phosphor-icons/react, and let tree shaking ship only the icons you import. Note the JSX attribute names: strokeWidth and strokeLinecap rather than the hyphenated HTML forms.

Mistakes that cost time

  1. Missing viewBox. The icon will not scale. Always keep it.
  2. Setting width and height only in HTML. Set size in CSS so it is responsive and overridable.
  3. Using an img tag then wanting hover colours. Decide styling needs before choosing the method.
  4. Duplicate IDs across inline SVGs. Gradient and clip path IDs collide and icons render wrongly. Prefix them.
  5. Serving a sprite from a CDN on another origin. <use> is blocked cross-origin. Keep it local.
  6. Shipping unoptimised design tool exports. Editor metadata can triple the file size. See optimising SVG.

Frequently asked questions

Inline SVG for interface icons that need to change colour or respond to state, as long as the count per page is modest. An SVG sprite once you have a larger set used across many pages, since it gives one cached request while keeping CSS colour control. An img tag for fixed graphics such as logos, and a CSS background or mask for purely decorative marks.
Inline when the icon needs CSS control: recolouring, hover and focus states, dark mode or animation. Use an img tag when the icon is a fixed graphic that never changes, since it caches separately and keeps your HTML clean. The deciding question is whether your stylesheet needs to reach inside the icon, because with an img tag it cannot.
Around twenty per page is a reasonable threshold, though it depends on icon complexity and whether the same icons repeat. Past that, the duplicated markup starts to add meaningful weight to a document that is not cached separately, and an SVG sprite becomes the better trade because it gives one cached request while preserving colour control.
It depends on the method and the role. An SVG in an img tag needs an alt attribute: real text if meaningful, empty if decorative, never omitted. An inline SVG that is decorative or sits next to visible text should carry aria-hidden="true". An inline SVG that conveys meaning on its own needs role="img" and an aria-label, or an accessible name on its parent button.
The use element is subject to cross-origin restrictions, so referencing a sprite hosted on a different origin is blocked by the browser. Serve the sprite from your own domain. This is the most common reason a sprite works in local development and silently fails once assets are moved to a CDN.
Either write the icon as a small component returning the SVG markup, remembering that JSX uses camelCase attributes such as strokeWidth, or install the official package for your icon set and import icons by name. Package imports are tree shaken, so you only ship the icons you actually use, which makes them the better choice once you need more than a handful.

Get an icon in the format you need

Copy inline SVG, download a file, or grab React or CSS output. Free, no sign-up.

Open the ICON OOP tool