Skip to content

Duotone icons, and how to colour them

Five shield-check icons with the shield tint rising from 0 to 100 percent

A duotone icon is drawn in one colour at two strengths. The part that carries the meaning, the tick on a shield or the body of a rocket, is at full strength, and a supporting part, the shield itself or the rocket's fins, sits behind it at a fraction of that strength. Because both tones come from one colour, a duotone icon recolours as easily as a line icon while looking a little richer.

Below are six icons in plain stroke and in Pikaicons' three duotone styles. Duo stroke pairs two line layers, duo solid makes one of the layers a filled shape, and contrast puts a full-strength outline over a soft fill of the same shape.

Stroke
Duo Stroke
Duo Solid
Contrast
The same six icons in stroke and the three duotone styles. The outlines barely change between rows, so all four read as one icon; only the weight behind them shifts.

Duotone, two-tone or duocolor?

The names are used loosely, so it helps to sort icons by how the second layer is built rather than by what the library calls it. There are three common builds:

  • One colour at two opacities. Both layers use the text colour and the secondary one is faded. This is what most libraries mean by duotone: Pikaicons draws the second layer at 28%, Phosphor at 20%, Google's Material Icons at 30% and Font Awesome at 40%.
  • Two separate colours. Each layer has its own colour, often a brand colour and a pale partner. Most one-colour duotone icons can be turned into this with a line of CSS, as shown further down.
  • Colours baked into the file. Each shape has a fixed hex fill. These look exactly as drawn but have to be edited by hand for every theme, which makes them closer to small illustrations than to UI icons.

Some libraries sell more than one two-layer style under different names. Untitled UI Pro lists Duocolor and Duotone as separate styles, and Hugeicons Pro has Duotone, Twotone and Bulk. The name alone won't tell you how the layers are built, so look at a downloaded SVG before you plan a colour system around it.

Inside a duotone SVG

This is exactly what the free icon page puts on your clipboard for the shield-check icon in duo stroke. There are two paths, both in currentColor, and the first one carries opacity="0.28".

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
  <!--
    Pikaicons: shield-check (Duo Stroke), free for personal and commercial use (https://pikaicons.com/free-icons)
    Credit is optional but appreciated: "Icons by Pikaicons" linking to https://pikaicons.com
    License: https://pikaicons.com/license
  -->
  <path opacity="0.28" transform="translate(3.38 2.19)" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M7.5 0.18L2.11 2.12C0.97 2.54 0.18 3.61 0.14 4.83L0.01 8.14C-0.15 12.24 1.99 16.09 5.55 18.12L7.07 18.98C7.97 19.5 9.08 19.51 9.99 19.02L11.48 18.21C15.29 16.14 17.54 12.02 17.2 7.69L16.98 4.74C16.89 3.57 16.11 2.55 15 2.15L9.54 0.18C8.88 -0.06 8.16 -0.06 7.5 0.18Z"/>
  <path transform="translate(9.13 9.73)" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M0 2.29L2.01 4.29C2.99 2.57 4.37 1.12 6 0"/>
</svg>

The faded path is the shield and the full-strength path is the tick. That order is the rule every duotone Pikaicon follows: the part you need to read the icon gets full strength, and the container or backdrop gets the soft tone. Since both use currentColor, the icon takes whatever text colour surrounds it, so it turns red in an error message and white on a dark button with no extra setup.

Where a second tone earns its place

Duotone icons do their best work where an icon is content rather than a control. Feature lists, empty states, onboarding steps, pricing tables, settings section headers and marketing pages all give an icon room to be looked at, and the second tone gives it a little more presence than a bare line.

They help less in dense, repeated UI. A toolbar of twenty duotone icons is busier than the same toolbar in stroke, and at 16px the soft layer has so few pixels that it reads as a smudge. Font Awesome's own guidance is to render duotone icons larger, starting at twice your base type size, which lines up with the 32px the icon sizes guide suggests for empty states and feature cards.

A dependable split is stroke or solid for controls and duotone for icons that illustrate something. The outline vs filled guide covers the control side, including when to fill an active tab.

Where to get duotone icons

Pikaicons is our library, so weigh its row accordingly. Every library below offers at least one style built from a full-strength layer and a softer one:

LibraryDuotone stylesFree?How the second tone is set
PikaiconsDuo stroke, duo solid, contrast500+ free icons in every style; the full 5,000+ across five styles from $99 oncecurrentColor at 28%, via an opacity attribute
PhosphorDuotone (one of six weights)Yes, MITcurrentColor; the background layer at 20% (example file)
Material IconsTwo toneYes, Apache 2.0, but no longer updated since 2022Black at 30% in the SVGs (example file); add fill="currentColor" to recolour
Font Awesome ProDuotone, Sharp DuotoneNo, Pro planCSS variables; secondary at 40%
Untitled UI ProDuocolor, DuotoneNo, the free version is line onlyNot stated on the icons page
Hugeicons ProDuotone, Twotone, BulkNo, Pro onlysecondaryColor prop

Pikaicons' catalogue is smaller than Font Awesome's, and it has no npm package or CDN: you copy icons from the free icon page or, on a paid plan, from the Pikaicons app as SVG, React, Vue, Svelte, React Native or Solid code. If you need a duotone set you can install from npm for free, Phosphor's React package has one. For more on each library, see the best free icon libraries and the best paid icon libraries.

Recolouring with one CSS rule

The faded layer is set by an SVG presentation attribute, and the SVG 2 spec gives those attributes a specificity of 0. Any CSS rule outranks them, so one selector can find every secondary layer and repaint it. Pick a palette below to see it applied to the same icons.

Duo Stroke
Duo Solid
Contrast
The same SVGs, recoloured by one CSS rule. 'One colour' is how they ship; the other palettes set the secondary layers to a second colour at full opacity.

The rule behind those palettes is short. The second selector only applies when you add the two-tone class, so one stylesheet covers both looks:

/* Both tones in one colour, as the icons ship. */
.icon {
  color: var(--icon-primary, currentColor);
}

/* Two colours: repaint the secondary layers at full strength. */
.icon.two-tone [opacity] {
  color: var(--icon-secondary);
  opacity: 1;
}

The selector matches on the opacity attribute rather than on a class, so it works on any duotone SVG drawn that way, including Phosphor's. Setting color on the path works because the path paints with currentColor, which always means the element's own colour.

The same rule in Tailwind

Tailwind's arbitrary variants express that selector inline, with an underscore standing in for the space. The text colour sets the primary tone and the variant sets the secondary:

<svg class="size-6 text-sky-900 [&_[opacity]]:text-sky-300 [&_[opacity]]:opacity-100">
  <!-- paste the icon's paths here -->
</svg>

Leave the two variant classes off and the icon keeps its shipped look in text-sky-900. Stack dark: in front of either class to give the dark theme its own pair.

A two-colour prop in React

In React the same idea becomes a prop. Pass the colours as CSS variables and let the stylesheet above do the work, so the icon's paths never change:

export function DuoIcon({ primary, secondary, size = 24, children }) {
  return (
    <svg
      className={secondary ? "icon two-tone" : "icon"}
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      aria-hidden="true"
      style={{ "--icon-primary": primary, "--icon-secondary": secondary }}
    >
      {children}
    </svg>
  );
}

Leave secondary out and the icon stays single-colour, still following the text around it, so one component covers buttons and feature lists alike. In TypeScript the custom properties need a cast (as React.CSSProperties). The guide to SVG icons in React compares this inline approach with sprites, <img> tags and SVGR.

Icons outside the DOM: img tags and sprites

The CSS rule only reaches SVG that sits in the page's own markup. An icon loaded with an <img> tag is a separate document, so it can't inherit your text colour or be matched by your selectors, and currentColor falls back to the file's default, which is usually black.

A sprite drawn with <use> sits in between. The SVG 2 spec says page styles can affect a <use> copy only by changing inherited values, and CSS custom properties are inherited. So if you edit the secondary layer in your sprite to read its colour and opacity from variables, you can set them from outside:

<!-- In the sprite: the secondary layer reads two variables, with the shipped look as fallback. -->
<symbol id="shield-check" viewBox="0 0 24 24" fill="none">
  <path style="color: var(--icon-secondary, currentColor); opacity: var(--icon-secondary-opacity, 0.28)" … />
  <path stroke="currentColor" … />
</symbol>

<!-- On the page: pick the second colour per use. -->
<svg class="icon" style="--icon-secondary: #7cc4fa; --icon-secondary-opacity: 1">
  <use href="/icons.svg#shield-check" />
</svg>

The edit is one search and replace across the sprite, and it also works for inline SVG, which makes it the most portable version of the technique.

Duotone in dark mode

Single-colour duotone needs no dark-mode work at all: switch the text colour and both tones follow. The soft layer does read a little stronger on dark, though. Near-black #111 at 28% on white has a contrast of about 1.9:1, while white at 28% on #111 comes to about 2.5:1, so a dark theme makes the backdrop layer more visible without any change to the file.

Two-colour palettes need a pair per theme, because a pale secondary that sits quietly on white glows on black. Swapping the two works well: the Ocean palette above uses #0b4f8a over #7cc4fa on light, and the reverse on dark. The CSS light-dark() function, Baseline since May 2024, keeps both pairs in one rule:

:root {
  color-scheme: light dark; /* light-dark() needs this */
}

.icon.two-tone {
  color: light-dark(#0b4f8a, #7cc4fa);
}

.icon.two-tone [opacity] {
  color: light-dark(#7cc4fa, #0b4f8a);
  opacity: 1;
}

What the faint layer can't carry

WCAG's non-text contrast criterion asks for 3:1 against the background for the parts of a graphic needed to understand it, and it exempts parts that are purely decorative. A 28% layer at about 1.9:1 fails that bar, so it must never be the only part that tells you what the icon is. That's why duotone icons put the meaning in the full-strength layer: #111 on white is about 18.9:1, and the shield without its backdrop is still a shield with a tick.

The same check applies to two-colour palettes. In Ocean, the primary #0b4f8a is 8.4:1 on white and the pale #7cc4fa is 1.9:1, which is fine for a backdrop and wrong for anything that carries meaning. Also avoid using the second colour alone to signal a state, such as a green backdrop for "done": WCAG 1.4.1 says colour can't be the only cue.

For screen readers, treat a duotone icon like any other. Hide it with aria-hidden="true" when a visible label sits next to it, and give it role="img" and an aria-label when it stands alone.

Duotone icons in Figma

In Figma the secondary tone is a separate layer whose layer opacity is set to 28%, which is why the SVG export carries an opacity attribute. Select that layer inside an instance and you can override its fill like any other nested layer, and setting its opacity to 100% gives you the two-colour look.

For a two-colour set, bind both fills to colour variables rather than hex values, so a theme or mode change updates every icon at once. To make your own duotone icon from an outline one, split the container shape into its own layer and drop its opacity; keep the meaning in the layer you leave at full strength.

The Pikaicons Figma file comes with every paid plan. With the free icons, copy the SVG from the free icon page and paste it onto the canvas, and the two layers arrive intact.

Frequently asked questions

What is a duotone icon?

A duotone icon is drawn in one colour at two strengths: the part that carries the meaning at full strength and a supporting part, usually a backdrop or container, at a lower opacity. In SVG both layers normally use currentColor, so the icon follows the surrounding text colour.

Are duotone and two-tone icons the same thing?

Often, but not always. Duotone usually means one colour at two opacities, while two-tone or duocolor can mean two separate colours, and some libraries sell both as different styles. A one-colour duotone icon can be turned into a two-colour one with a CSS rule.

Where can I get free duotone icons?

Phosphor has a free duotone weight under the MIT license, and Google's classic Material Icons include a Two tone theme, though the set is no longer updated. Pikaicons has 500+ free icons in duo stroke, duo solid and contrast, free for commercial use, as the free icons for commercial use guide explains.

How do I change the colours of a duotone SVG icon?

Set color on the icon to change both tones at once. For a second colour, target the faded layer, in plain CSS with .icon [opacity] and in Tailwind with [&_[opacity]]:, then set its color and opacity: 1. This only works when the SVG is inline in the page, or in a sprite whose secondary layer reads CSS variables.

Are Font Awesome duotone icons free?

No. Font Awesome's Duotone and Sharp Duotone styles need a Pro plan or Pro license, and their colours and opacities are set with CSS variables such as --fa-secondary-color.

Do duotone icons work at 16px?

They work, but the soft layer gets very few pixels at 16px and tends to blur into the main shape. Use stroke or solid for small controls and keep duotone for icons at 24px and up, ideally 32px.

The quickest way to see how the second tone behaves in your own UI is to try it there. Switch the free icon page to duo stroke, duo solid or contrast, copy a few icons as SVG or JSX, and drop them into a component that uses the CSS above.

Ash