HTML SVG icons

Paste the icon markup inline, size it in em, and colour it with currentColor. That covers most pages. A symbol sprite is the upgrade once the same icons repeat across a long document.

To use SVG icons in HTML, paste the icon markup inline, set its size in em and its colour to currentColor. The icon then follows the surrounding text on both counts.

<button>
  <svg viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor" aria-hidden="true">
    <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none"/>
  </svg>
  Add row
</button>
A button containing an inline SVG icon and a text label, with the icon matching the label colour and height.
A button containing an inline SVG icon and a text label, with the icon matching the label colour and height.

The three delivery methods

Method Styleable from CSS Best for
Inline <svg> Yes A handful of icons, or any icon you restyle
<symbol> sprite plus <use> Yes The same icons repeated many times
<img src="icon.svg"> No Logos and fixed art that never change colour

Icon fonts are a fourth option that still works. They download a whole typeface for a few glyphs and fall back to empty boxes when the font fails, so most sets have moved to SVG.

Sizing in em rather than pixels

Pixel sizes look right in one place and wrong two components later. Relative sizing removes the maintenance.

.icon {
  width: 1em;
  height: 1em;
  fill: currentColor;
  vertical-align: -0.125em;
  flex: none;
}

vertical-align fixes the icon sitting slightly high against the baseline. flex: none stops it being squashed when it is the first child of a flex row with long text beside it.

For an icon that should be larger than its label, set font-size on the icon rather than a pixel width. It stays proportional when the page is zoomed.

The sprite pattern

Repeat the same eight icons across a long document and inline markup becomes most of the file. Define each one once.

<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <symbol id="i-check" viewBox="0 0 24 24">
    <path d="M20 6 9 17l-5-5" fill="none" stroke="currentColor" stroke-width="2"/>
  </symbol>
  <symbol id="i-alert" viewBox="0 0 24 24">
    <path d="M12 3 2 21h20L12 3zm0 6v6m0 3v.01" fill="none" stroke="currentColor" stroke-width="2"/>
  </symbol>
</svg>

Then each use is one short tag:

<svg class="icon"><use href="#i-check"/></svg>
<svg class="icon" style="color:#ef4444"><use href="#i-alert"/></svg>

The viewBox lives on the symbol, so the instances do not need one. Because the paths use currentColor, each instance takes the colour of its own context, which is how changing SVG colour works at set level.

Dev tools with a use element selected, showing the shadow content resolved from the symbol defined earlier in the document.
Dev tools with a use element selected, showing the shadow content resolved from the symbol defined earlier in the document.

Accessibility in three rules

  1. Decorative icon beside a text label. aria-hidden="true" on the svg. The label already says it.
  2. Icon alone in a button or link. Name the control: <button aria-label="Delete row">. See aria-label for the wording.
  3. Icon carrying meaning in a table cell, a status tick for example. Give the svg role="img" and an aria-label, or put visually hidden text beside it.

Never leave an icon only button unnamed. A screen reader reaches it and announces "button", with nothing else to go on.

Hover and state without extra markup

Because the icon inherits color, state changes are one rule on the parent.

.row-action { color: #94a3b8; transition: color 150ms ease; }
.row-action:hover,
.row-action:focus-visible { color: #38bdf8; }

The transition belongs on the resting state, not inside :hover, or the icon snaps back the moment the pointer leaves. CSS hover transition covers why.

Cleaning up exported icons

Design tools add material you do not need. Before pasting, strip:

  • width and height attributes, so the icon scales to fit.
  • Hard coded fill="#000000", replaced with currentColor.
  • Generated id values like clip0_1_2, which collide when two icons land in the same page.
  • <title> and <desc> blocks left over from the file name, which screen readers will read aloud.

The id collision is the one that produces a real bug. Two icons with the same clipPath id in one document, and the second one renders the first one's clip.

Most exported icons do not need a clip path at all. If the shape is a simple outline, delete the clip and the group wrapping it, then check the icon still draws. Fewer nodes means fewer collisions later.

An exported icon's raw markup beside the cleaned version, with the removed attributes highlighted.
An exported icon's raw markup beside the cleaned version, with the removed attributes highlighted.

Alignment inside buttons and rows

Most icon bugs are alignment, not drawing. Two patterns cover nearly everything.

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
}

inline-flex with align-items: center removes the baseline problem entirely, so the vertical-align hack above is only needed when the icon sits in running text.

gap in em keeps the spacing proportional to the label. A fixed pixel gap looks tight at 12px text and loose at 20px.

For an icon only button, give it a square hit area rather than relying on padding around a 16 pixel glyph.

.icon-btn {
  display: inline-grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
}

That is roughly 36 pixels, which is close to the smallest comfortable touch target. Smaller than that and the control is hard to hit on a phone.

Consistency across a set

Icons from different sources rarely match, and the mismatch reads as sloppiness even when nobody can name the cause.

  • One viewBox size across the set, usually 24 by 24.
  • One stroke width, usually 1.5 or 2, and not mixed within a screen.
  • One corner treatment, either stroke-linecap="round" everywhere or nowhere.
  • The same optical weight. A solid icon beside line icons dominates the row.

If a borrowed icon does not match, redraw it rather than scaling it. A 16 by 16 icon scaled to 24 has a stroke that is half a pixel thicker than everything around it.

Checking the set in one place

Build a single page with every icon at three sizes, in light and dark, with one of them inside a button. Most problems are visible in that one view.

Paste it into a NOS document and keep the link. The markup renders as written, including the sprite, so the page works as a reference anyone on the team can open.

When an icon changes, edit the page. The address stays the same, so the link in the design notes does not need updating and nobody is looking at a stale export.

Questions people ask

Should I inline SVG icons or link to files?

Inline if you want to style them, which is usually. Linking with an img tag keeps the HTML shorter and lets the browser cache the file, but your page CSS cannot reach inside it, so the colour is fixed at export time.

Are icon fonts still a reasonable choice?

They work, and they are simple to size with font-size. The drawbacks are that the whole set downloads for a few glyphs, icons can render as boxes if the font fails, and screen readers may read the private use character. SVG avoids all three.

How do I make an icon match the text next to it?

Set width and height to 1em and fill to currentColor. The icon then scales with the font size and takes the text colour. A small negative vertical-align, around -0.125em, lines it up with the baseline.

What accessibility attributes do SVG icons need?

If the icon sits beside a text label, add aria-hidden true so it is not announced twice. If it is the only content of a button, put an aria-label on the button describing the action. Decoration should be silent, controls must be named.

Keep reading