Icons are the smallest colored things in an interface and they break the most rules. A palette that works beautifully on buttons and cards can produce icons that are invisible, muddy, or inconsistent — because a 16-pixel glyph made of 1.5-pixel strokes behaves nothing like a 200-pixel panel filled with the same color.
Small and thin needs more contrast, not the same contrast
A large filled shape and a thin stroke in identical color are not equally visible. The stroke has less area, its edges are heavily anti-aliased, and at small sizes a 1px line may be rendered across two pixels at partial opacity — effectively lightening the color you specified.
WCAG asks for 3:1 for meaningful graphics, and for icons that is a floor rather than a target. If an icon carries meaning on its own — a close button, a status indicator, a navigation glyph with no label — give it more headroom than the minimum. Somewhere around 4.5:1 is a more comfortable working target, and it costs nothing.
Two situations where the minimum is genuinely not enough: icons at 16px or smaller, and icons drawn with hairline strokes. Both lose effective contrast to anti-aliasing. Test them in the contrast checker against every background they can appear on, then look at them at actual size — the number and the eye should agree, and when they do not, trust the eye and raise the contrast.
currentColor is the single most useful trick
Most icons should not have a color of their own. They should inherit one:
<svg viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M20 6 9 17l-5-5"/>
</svg>
currentColor resolves to the element's computed color value. An icon inside a button takes the button's text color. Inside a link, the link color. On hover, it changes with the text automatically. In dark mode it follows whatever the text does, with no separate rule.
This eliminates an entire class of bug — the icon that stayed dark gray when the surrounding text went light — and it removes dozens of one-off color declarations from a codebase.
Reserve explicit colors for icons where color is the information: a red error glyph, a green success tick, a multi-color brand logo. Everything else should inherit.
Inline SVG versus every other method
How you embed an icon determines whether you can color it at all.
Inline SVG — the markup sits in the document, so CSS can style it and currentColor works. This is the method to use for interface icons.
<img src="icon.svg"> — cached and simple, but the SVG is an isolated document. CSS cannot reach inside it and currentColor resolves to the SVG's own default. Fine for a fixed-color logo, unusable for anything that needs to respond to state.
CSS mask-image — a middle path. The SVG defines the shape, and background-color supplies the color:
.icon {
width: 24px; height: 24px;
background-color: currentColor;
mask-image: url(icon.svg);
mask-size: contain;
}
This keeps files cacheable while remaining colorable, at the cost of losing multi-color icons entirely — everything becomes a silhouette.
Icon fonts — an older approach. They color like text, which is convenient, but bring accessibility problems and render at font rasterisation quality rather than as vectors. Inline SVG has largely replaced them.
Multi-color icons: two-tone, not full-color
Where an icon genuinely needs two colors — a filled shape with an accent — the maintainable approach uses custom properties with sensible fallbacks:
<svg viewBox="0 0 24 24">
<path fill="var(--icon-bg, currentColor)" d="..." />
<path fill="var(--icon-accent, #3B82F6)" d="..." />
</svg>
The primary shape still inherits by default, and the accent can be themed per context. A common and effective pattern is a solid shape at low opacity of the brand color plus a full-strength detail, giving depth without a second hue — the tint generator produces suitable lighter variants.
Keep multi-color icons to two colors. Three-color icon sets are difficult to keep consistent, hard to theme, and tend to look busy at small sizes.
States and consistency
Icons need the same state coverage as the controls they sit in: default, hover, active, focused, disabled, and selected. If the icon inherits currentColor, most of these come free from the parent's text color — which is the strongest argument for inheritance.
Where an icon does carry its own color, derive the states from the base rather than picking each by hand. The shade generator gives a consistent darker step for hover and active, and color-mix() can do it in CSS directly:
.icon-button:hover .icon {
color: color-mix(in oklab, var(--icon-color) 85%, black);
}
Disabled icons are the one case permitted to fail contrast, because reduced visibility is the message. Do not take that as license to make them illegible — and never rely on color alone to indicate that something is disabled.
Status icons must not depend on hue
A green tick and a red cross are the classic example of information encoded in hue. For a reader with red-green color-vision deficiency, and both glyphs are the same shape at a glance, the distinction disappears.
Icons are actually well placed to solve this, because shape is a second channel that comes free. A tick and a cross are already different shapes; the mistake is using the same shape in two colors — two identical circles, one green and one red. Make the shapes differ, keep the colors as reinforcement, and the meaning survives.
Check any status icon set in the color blindness simulator. If two states look identical there, the shapes are not doing enough work. Our accessible color guide covers the general principle.
Icons on images and colored backgrounds
The hardest case: an icon over a photograph, where the background varies and any fixed color fails somewhere.
Options, roughly in order of reliability. Give the icon a solid container — a circular button with its own background is the most robust answer and the one most interfaces settle on. Add a subtle scrim, a semi-transparent dark or light layer beneath the icon area. Or add a contrasting stroke or drop shadow around the glyph, which works but can look heavy at small sizes.
What does not work is picking a color that "usually" reads. Photographs contain everything, and the one that breaks it will be a user upload.
Keeping a set coherent
Consistency matters more than any individual choice. A few things to hold constant across a set: stroke width (mixing 1.5px and 2px is immediately visible), whether icons are stroked or filled, optical weight, and the base color source.
If a set mixes stroked and filled icons, some will look bolder than others at the same color, because filled shapes carry more visual weight. Either commit to one style, or accept that the filled ones will need to be lighter to balance.
A checklist
- Interface icons use
currentColorand inherit from their parent. - Explicit colors are reserved for icons where color carries meaning.
- Every icon clears 3:1 against every background it appears on — more for small or hairline icons.
- Status is distinguished by shape as well as hue.
- Hover and active states are derived from the base color, not hand-picked.
- Icons over images have a container, a scrim, or an outline.
- Stroke width and fill style are consistent across the set.
- The set has been checked at actual size, not zoomed in.
That last point catches more problems than any of the others. Icons are always designed at 400% and shipped at 100%.