ICON OOP
Icons & Logos
Guide

Building an icon system that survives

Every product starts with a few icons and ends with three different settings gears, two trash cans and a search icon from a library nobody remembers installing. The fix is not better taste. It is a short document and one rule about who can add an icon.

HomeGuidesIcon systems

Icon drift is one of the most predictable problems in product design, and one of the least discussed. It happens the same way in every organisation: nobody makes a bad decision, and the result is bad anyway.

Developer A needs a settings icon and picks settings. Developer B, three months later on a different screen, searches and picks adjustments because it looked better in context. Developer C imports a component library that ships its own icons. None of them did anything wrong. The product now has three settings icons, and users have learned none of them.

What actually fixes this

A one-page icon inventory: concept, icon name, where it is used. A rule that adding a new icon requires updating the inventory. That is it. Everything below is detail on how to do those two things well, but the inventory alone prevents most of the damage.

The inventory, which is the whole thing

An icon inventory is a table with four columns. It can live in a markdown file in your repo, a page in your design system, or a spreadsheet. Format matters far less than existing.

ConceptIcon nameUsed inNotes
SettingssettingsNav, account menuNever use adjustments
Deletetrash-2Row actions, detail headerAlways paired with confirm
SearchsearchGlobal header, filters
External linkarrow-up-rightAll outbound links12px, after the text

The value is in the first column. Listing icons is useless; listing concepts is what stops duplication, because the next person searches for the concept and finds the decision already made.

The "never use" note in the third row is doing more work than it looks. Recording the rejected alternative is what prevents someone re-litigating the choice in six months, which is the most common way inventories decay.

Rule one: one library

Pick one UI icon set and take everything from it. The comparison page covers choosing; the point here is that the decision must be recorded and enforced rather than assumed.

Two legitimate exceptions:

  • Brand logos. These identify real companies and are not part of your UI icon language. Mixing them in is expected. See brand logos.
  • Emoji and reactions, where the whole point is expressive colour.

The failure mode to watch is not a designer deliberately importing a second library. It is a component library, a charting package or an admin template arriving with its own icons attached. Audit your dependencies, not just your design files.

Naming: semantic or literal

Two approaches, and the mistake is mixing them.

Literal names describe the picture: trash, gear, magnifier. This is how icon libraries name things, because a library does not know what you will use an icon for.

Semantic names describe the purpose: delete, settings, search. This is how a design system should name things, because it survives the underlying icon changing.

The practical pattern is a thin alias layer: semantic names in your code, mapped to library names in one place.

// icons.ts
export { Trash2 as DeleteIcon,
         Settings as SettingsIcon,
         Search as SearchIcon,
         ArrowUpRight as ExternalIcon } from 'lucide-react';

Now changing which icon represents deletion is a one-line change, and no component in your codebase knows or cares which library you use. This also makes migrating icon sets a mapping exercise rather than a codebase-wide find and replace, which is the difference between a half-day job and a fortnight.

Choosing a size scale

Three sizes. Not five, not "whatever fits".

TokenSizeUsed for
icon-sm16pxInline with body text, table rows, dense lists
icon-md20pxButtons, form fields, most UI. The default
icon-lg24pxNavigation, page headers, primary actions

Add a fourth only if you have illustrative icons in empty states or feature grids, typically 40 or 48px, and treat it as a separate category rather than an extension of the scale.

Two things people miss. Stroke weight should not be constant across the scale: a 2px stroke that looks right at 24px looks heavy at 16px and thin at 48px. And tap target size is independent of icon size: a 16px icon in a toolbar still needs at least a 44 by 44 pixel hit area. The icon is not the button. Detail in icon sizes.

Defining states once

Define your icon states as tokens and never set an icon colour ad hoc again.

:root {
  --icon-default:  #6B7280;
  --icon-hover:    #111827;
  --icon-active:   #FF5A36;
  --icon-disabled: #D1D5DB;
  --icon-danger:   #DC2626;
  --icon-success:  #059669;
}

Because icons use currentColor, applying these is setting color, which means dark mode is a matter of redefining the variables and nothing else. See changing icon colour.

Also decide once how selected states are expressed: filled versus outline, colour change, or both. Whatever you choose, apply it everywhere. Inconsistent selected states are one of the fastest ways to make an interface feel unreliable.

Icons as design tokens

Once you have semantic names, sizes and state colours defined, icons stop being assets and become tokens, the same as your type scale or spacing scale. That has one important consequence: designers and developers use the same vocabulary.

A design review comment becomes "use DeleteIcon at icon-sm with --icon-danger" rather than "make the bin smaller and red". That precision is most of what a design system buys you, and it is available for icons far more cheaply than for most other parts of a system.

Who is allowed to add an icon

The governance question, and the honest answer is that a light rule beats a heavy process.

What does not work: a design lead approving every icon. It becomes a bottleneck, people route around it, and the inventory rots.

What works: anyone may add an icon, but the pull request must update the inventory. It is a thirty second addition and it forces the two questions that matter: does a concept for this already exist, and is this icon from our library?

Add a lightweight check for the third failure mode, since it is the one people forget: removing the last use of an icon should remove it from the inventory too. Otherwise the inventory grows monotonically and stops reflecting reality, at which point people stop trusting it.

Auditing an existing product

If you already have drift, a half-day audit fixes most of it.

  1. Find every icon. Grep for icon imports, SVG elements and icon class names. Include dependencies, which is where surprises live.
  2. Group by concept, not by file. This is where the three settings icons become visible.
  3. Pick one per concept. Where two are genuinely different concepts, name both clearly. Where they are not, choose and record the rejected one.
  4. Identify foreign libraries. Anything not from your chosen set gets replaced or explicitly justified.
  5. Write the inventory as you go. The audit produces it as a by-product; do not treat it as a separate task afterwards, because it will not happen.
  6. Fix in one pass. Icon consistency changes are low risk and high visibility, which makes them unusually easy to ship as a single change.

The audit is worth doing once. The inventory is what stops you needing to do it again.

Frequently asked questions

A short table listing each concept in your product, the exact icon name chosen for it, where it is used, and any rejected alternatives. It is the single most effective thing you can do about icon consistency, because it turns icon choice into a decision made once rather than re-made by whoever is building the next screen.
Three for interface use: roughly 16px for inline and dense contexts, 20px for buttons and most UI, and 24px for navigation and headers. Add a fourth only for illustrative icons in empty states, and treat that as a separate category. More sizes than this produces inconsistency without adding any expressive capability.
Semantically in your codebase, literally in the library. Icon libraries name by picture because they cannot know your use case, so add a thin alias layer mapping your semantic names such as DeleteIcon to the library's literal names. Changing which icon represents an action then becomes a one-line change instead of a codebase-wide find and replace.
Keep an inventory organised by concept rather than by icon, and require that adding an icon updates it. Recording the rejected alternatives matters as much as recording the chosen icon, since that is what stops the decision being re-litigated later. Also audit your dependencies, because component and charting libraries frequently arrive with their own icons attached.
No. A stroke that looks correct at 24 pixels appears heavy at 16 and thin at 48, because browsers scale stroke proportionally with the icon. Increase stroke slightly for small sizes and reduce it for large ones. Some libraries offer an absolute stroke width option that keeps the rendered pixel width fixed regardless of icon size.
Anyone, provided the change updates the icon inventory. Requiring design approval for every icon creates a bottleneck that people route around, which is worse than no process. The inventory update takes thirty seconds and forces the two questions that matter: does a concept for this already exist, and is this icon from our chosen library?

Build your icon set from one library

Search seven collections, stay inside one, and export a consistent set. Free, no account.

Open the ICON OOP tool