CSS Icons

An icon is a small graphic that communicates meaning at a glance — a magnifying glass for search, a trash can for delete, a heart for favorite. CSS itself does not draw icons, but it gives you several ways to display and style them: as text characters, as background images, or as color-controlled shapes. Choosing the right technique affects how sharp your icons look at every screen size, how easily you can recolor them, and how accessible your page is.

This lesson covers every mainstream approach to putting icons on a page using CSS: Unicode/emoji glyphs, icon fonts, <code>background-image</code> with SVG, and the modern <code>mask-image</code> technique — plus the sizing, positioning, and accessibility rules that apply no matter which method you pick.

Overview: How CSS Icons Work

“CSS icons” is not a single feature — it is a collection of techniques that all lean on things CSS already does well: rendering text glyphs, painting background images, and now, masking shapes. Understanding the rendering model behind each technique tells you when to use it.

Text-based icons (Unicode and icon fonts)

Every character your browser renders, including symbols like <code>★</code> or <code>→</code>, is a glyph drawn by whatever font is active on that element. Icon fonts (historically popular libraries bundle hundreds of icons) exploit this: each icon is mapped to a Unicode code point in a custom font file, so the browser’s text-rendering pipeline draws it exactly like a letter. Because it is text, it inherits <code>color</code>, <code>font-size</code>, and text-shadow for free, and it scales crisply at any resolution since fonts are vector outlines.

Image-based icons (background-image)

Here the icon is a raster (PNG) or vector (SVG) image painted into the element’s background layer, positioned and sized with <code>background-position</code> and <code>background-size</code>. The browser’s rendering engine treats it exactly like a photo background — it does not know or care that the image “is” an icon. This means the image’s own internal colors are fixed; CSS cannot recolor a plain background-image on hover or in dark mode without swapping the file.

Mask-based icons (mask-image)

The newest and most flexible technique separates shape from color. <code>mask-image</code> tells the rendering engine to use an image (typically a single-color SVG) purely as an alpha channel: wherever the mask is opaque, the element’s own <code>background-color</code> shows through; wherever it’s transparent, nothing is painted. Because the color comes from a normal CSS property, you can change it with <code>:hover</code>, media queries, or custom properties, while keeping the crispness of a vector shape.

Syntax

There is no single “icon” property. Instead, you combine a handful of properties depending on the technique:

Property Used for Notes
content Injects a Unicode character via ::before/::after Required on pseudo-elements or nothing renders
font-family Selects the icon font that maps code points to glyphs Must match the loaded @font-face name
background-image Paints an SVG or PNG icon into the background layer Pair with background-size and background-repeat
background-size Controls the rendered dimensions of the icon image contain keeps aspect ratio, fitting inside the box
mask-image / -webkit-mask-image Uses an image as a shape mask, filled by background-color Needs the -webkit- prefix in some browsers
width, height Sizes the icon box Required for empty elements, which have no intrinsic size

Examples

Example 1: A Unicode symbol as an icon

.icon-star::before {
  content: "\2605";
  color: gold;
  font-size: 1.5rem;
  margin-right: 0.5rem;
}

Applied to <span class=”icon-star”></span>Favorite. Result: a solid gold five-pointed star character appears immediately before the word “Favorite”, sized larger than the surrounding text.

The <code>content</code> value <code>\2605</code> is the CSS escape for the Unicode “BLACK STAR” code point. Because this is rendered as text, <code>color</code> and <code>font-size</code> control it exactly like any other character — no image file is involved.

Example 2: An SVG background-image icon in a button

.btn-download {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  background-color: #2563eb;
  color: #ffffff;
  border: none;
  border-radius: 6px;
}

.btn-download::before {
  content: "";
  display: inline-block;
  width: 20px;
  height: 20px;
  background-image: url("data:image/svg+xml;utf8,");
  background-size: contain;
  background-repeat: no-repeat;
}

Result: a blue, rounded-corner button appears with a small white downward-pointing arrow icon to the left of its label text.

The icon is embedded directly as a data URI, so no extra network request is needed. <code>content: “”</code> generates the pseudo-element (an empty box), <code>display: inline-block</code> gives it dimensions, and <code>background-size: contain</code> scales the SVG to fit the 20x20px box without distortion.

Example 3: A recolorable icon with mask-image

:root {
  --icon-heart: url("data:image/svg+xml;utf8,");
}

.icon-heart {
  display: inline-block;
  width: 24px;
  height: 24px;
  background-color: crimson;
  -webkit-mask-image: var(--icon-heart);
  mask-image: var(--icon-heart);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.icon-heart:hover {
  background-color: darkred;
}

Result: a solid crimson heart shape renders at 24 by 24 pixels; when the pointer hovers over it, the same heart shape instantly turns dark red.

The SVG only defines a shape (a heart-shaped path with no fill color that matters); the mask uses its alpha channel as a stencil. The visible color always comes from <code>background-color</code>, which is why a single icon file can appear in any color the design needs, including on hover or in a dark theme, without editing the SVG.

How It Works Step by Step

When the rendering engine encounters an icon rule, it processes it as part of normal box generation and painting:

For pseudo-element text icons

  1. The engine evaluates <code>content</code> on <code>::before</code>/<code>::after</code>. If present (even empty string), a generated box is inserted into the render tree as if it were a real child element.
  2. The box’s <code>font-family</code> and <code>font-size</code> are resolved through the normal cascade, then the specified Unicode code point is looked up in that font’s glyph table.
  3. The resulting glyph outline is painted in the current <code>color</code> value, exactly like any text run.

For background/mask image icons

  1. The element’s box is sized first, using its <code>width</code>/<code>height</code> (or intrinsic content size if any).
  2. The image referenced by <code>background-image</code> or <code>mask-image</code> is decoded — for a data URI, this happens immediately with no network round trip.
  3. <code>background-size</code>/<code>mask-size</code> computes how the image is scaled to fit the box (<code>contain</code> preserves aspect ratio; the browser picks whichever dimension constrains first).
  4. For a mask, the engine treats every pixel’s alpha value as an opacity multiplier applied to the element’s own painted background (its <code>background-color</code>), rather than painting the image’s own colors.

Common Mistakes

Mistake 1: Forgetting <code>content</code> on a pseudo-element icon. Without it, the pseudo-element is never generated at all, so nothing else on the rule has any effect.

.icon-cart::before {
  background-image: url("cart.svg");
  width: 20px;
  height: 20px;
}

Corrected — add <code>content: “”</code> and give it a box context:

.icon-cart::before {
  content: "";
  display: inline-block;
  background-image: url("cart.svg");
  width: 20px;
  height: 20px;
}

Mistake 2: Sizing a background icon incorrectly. An empty element with no explicit dimensions and no <code>display</code> change collapses to zero size, so a background image never becomes visible even though the rule is otherwise valid.

.icon-alert {
  background-image: url("alert.svg");
  background-repeat: no-repeat;
}

Corrected — set a display mode that accepts dimensions, plus explicit size and fit:

.icon-alert {
  display: inline-block;
  width: 20px;
  height: 20px;
  background-image: url("alert.svg");
  background-size: contain;
  background-repeat: no-repeat;
}

Best Practices

  • Use <code>mask-image</code> or inline SVG when an icon needs to change color on hover, focus, or in dark mode — plain <code>background-image</code> icons cannot be recolored with CSS alone.
  • Always set explicit <code>width</code> and <code>height</code> on icon elements or pseudo-elements; empty boxes have no intrinsic size to fall back on.
  • Prefer SVG over PNG for icons — it stays sharp at any zoom level or screen density, and can be inlined as a data URI to avoid extra requests.
  • If an icon conveys meaning that isn’t otherwise in the text (a lone trash-can icon button, for example), add an accessible label in the HTML, such as <code>aria-label</code>, since CSS-generated content is unreliable for screen readers.
  • Keep icon fonts and CSS-generated icons purely decorative when real text already conveys the message, so removing the CSS doesn’t remove information.
  • Use CSS custom properties (like <code>–icon-heart</code>) to store repeated SVG data URIs once and reuse them across rules.

Practice Exercises

Exercise 1: Create a <code>.icon-check::before</code> rule that displays a Unicode checkmark character in green, sized at <code>1.25rem</code>, placed before a list item’s text.

Exercise 2: Build a <code>.icon-close</code> class using <code>mask-image</code> with an SVG “X” shape, styled with a gray <code>background-color</code> by default that switches to red on <code>:hover</code>.

Exercise 3: Take the download button from Example 2 and modify it so the icon appears after the label text instead of before, and increase the icon size to 24 by 24 pixels.

Summary

  • CSS has no dedicated “icon” feature — icons are built from text glyphs, background images, or masked shapes.
  • Unicode and icon-font icons are text, so they inherit <code>color</code> and <code>font-size</code> automatically and stay crisp at any size.
  • <code>background-image</code> icons are simple but their internal colors are fixed inside the image file.
  • <code>mask-image</code> separates shape from color, letting a single SVG be recolored freely with <code>background-color</code>.
  • Pseudo-element icons require <code>content</code> to exist at all, and image-based icons require explicit sizing to be visible.
  • Purely decorative CSS icons should never be the only carrier of meaning — pair them with accessible text or labels.