Learning Track
Filter content by your role to see the most relevant sections
Icon Foundations
Systematize iconography: a four-step size scale aligned to the spacing rhythm, a single stroke weight, optical alignment, and the labeling contract that separates meaningful icons from decorative ones for assistive technology.
Why This Matters
Icons are the highest-frequency visual element in any interface and the fastest to decay. One designer ships 18px icons, another 20, a third 24—and within a quarter every screen uses all three. None of them is wrong; the inconsistency is wrong, because users read icon size, weight, and alignment as systematic signals whether designers intend it or not.
Iconography also has an accessibility obligation that is routinely botched: an icon is either meaningful—it communicates something no adjacent text carries, and must be labeled for assistive technology—or decorative, and must be hidden from it. The failure modes are silent either way: unlabeled meaningful icons vanish for screen reader users; labeled decorative icons announce noise, reading a star glyph aloud next to the word Favorite.
This page covers the icon system as built here: the icon.size scale and icon.strokeWidth in the core tokens, and the labeling contract implemented by the Icon component in ui/components/Icon.
Core Concepts: Scale, Stroke, Alignment, Meaning
Four Sizes, No Exceptions
The core scale in ui/designTokens/core/icon.tokens.json is four steps:
// icon.size (values)
sm: 16px md: 20px lg: 24px xl: 32pxFour is enough and four is the point: sm is inline with body text, md the default for buttons and controls (the Icon component's 20px default), lg for navigation and list affordances, xl for feature moments. The values align with the spacing scale's neighbors (16, 24, 32 are spacing.size.06/07/08), which is why icons sit correctly in padded containers without bespoke alignment CSS.
One Stroke Weight
icon.strokeWidth.default is 1.5px, and the word default in that name is doing honest work: it is the weight every custom-drawn icon ships at. Stroke consistency is what makes icons from different sources read as one family—two icons at the same size but different weights look like different typefaces. When an icon needs to feel bolder, the system answer is a filled variant or a heavier glyph from the set, not a locally bumped stroke.
Optical Alignment Beats Mathematical Alignment
Icons are drawn against their viewBox, but shapes read by their visual mass. A circle and a square at the same 20px box do not look the same size—the circle needs to overshoot slightly. The system's convention: icons are drawn on a consistent grid with a small safe area (roughly 10% padding inside the viewBox), and container alignment uses the box, not the ink. The payoff is that align-items: center just works across the whole set.
Meaningful or Decorative—Never Both
The accessibility contract is binary, and this repository's Icon component implements it directly:
// ui/components/Icon/Icon.tsx (behavior)
const hasLabel = typeof label === 'string' && label.length > 0;
<span
role={hasLabel ? 'img' : undefined}
aria-label={hasLabel ? label : undefined}
aria-hidden={hasLabel ? undefined : true}
>
<svg aria-hidden="true" focusable="false">…</svg>
</span>Pass label and the icon becomes role="img" with an accessible name—use this when the icon is the only carrier of meaning, like a stand-alone close button. Omit it and the icon is aria-hidden—correct for icons beside text that already says the thing. The important design of this API is that decorative is the default: in a well-composed interface most icons sit next to labels, and the safe default is silence.
System Roles: Where Icon Decisions Land
Design Impact
Designers own the glyph vocabulary—what concepts get icons, which style (this system uses a solid set via the FontAwesome kit plus custom SVG sprites), and the grid conventions that keep optical alignment. The review discipline is the same as color's: an icon in a comp must be able to name its size step and its meaning status.
Engineering Impact
Engineers own the contract: one Icon component, sizes passed from the scale, never a raw <svg> inline in product code where the labeling logic can be bypassed. The component keeps the rules in one place—hydration quirks, currentColor inheritance, and the ARIA branch included.
Accessibility Impact
Accessibility owns the naming quality: a label is a name ("Close"), not a description ("gray x icon top right"). Screen reader output is the review surface— hearing "star, Favorite" is a bug in the decorative direction; hearing nothing on a label-only icon button is a bug in the meaningful direction.
Design & Code Interplay
In the design tool, the icon system appears as four named component sizes with the same names as the tokens, drawn on a shared grid with the 1.5px stroke as a library-wide style. The meaning-status decision shows up in the comp itself: a text-plus-icon pair is drawn knowing the icon will render decorative; a solo icon button is annotated with its accessible name, because in code that name is a required prop the designer is the only one qualified to write.
That last point is the quiet win of treating labeling as design data: the accessible name never gets invented by whoever implements the ticket.
In code, sizes flow from the scale into the one Icon component, and the ARIA contract is structural rather than per-usage:
/* Meaningful: the icon IS the affordance */
<Icon icon={faXmark} label="Close" width={20} height={20} />
// → <span role="img" aria-label="Close">
/* Decorative: text carries the meaning */
<button>
<Icon icon={faStar} width={16} height={16} />
Favorite
</button>
// → <span aria-hidden="true"> + readable "Favorite" buttonSizing from the scale in CSS-consuming contexts reads the emitted custom properties:
.inlineIcon {
width: var(--core-icon-size-sm, 16px);
height: var(--core-icon-size-sm, 16px);
}
/* Icon color always inherits — currentColor, never a fill token */
.inlineIcon svg { fill: currentColor; }Note what is not in the icon tokens: color. Icons inherit currentColor so they follow the text semantic of their context—an icon in a destructive button is destructive-colored by construction, and dark mode needs no icon-specific work at all.
Applied Example: An Icon-Only Toolbar
Ship the highest-risk icon pattern correctly: a toolbar of icon-only buttons, where every icon is meaningful by definition.
- Size from context: toolbar buttons are controls;
icon.size.md(20px) inside acontrol.size.sm(24px) height, with the hit area extended to the 44px floor. - Name every one: each Icon gets a
label—"Bold", "Italic", "Insert link". These names are also thearia-labelof the button, and the tooltip copy is the same string: one name, three surfaces. - State, not glyph-swapping: disabled and active states change color token and opacity, not the icon file—weight and shape stay constant so the toolbar reads as one system.
- Verify by ear: the acceptance test is a screen reader pass: the toolbar announces five buttons with five names and zero surprises. Visual review cannot catch this category of defect.
The pattern generalizes: wherever an icon stands alone, the label is mandatory and the review is auditory; wherever an icon accompanies text, silence is mandatory and the review is visual.
Constraints & Trade-offs
- Four sizes vs free resizing: a locked scale removes 18px and 28px, which will be requested. The answer is either a real usage gap (add a step with review) or drift (refuse)—and the scale's alignment with spacing steps is the tiebreaker.
- One stroke weight vs per-icon drama: marketing moments want heavier marks; product UI needs uniformity. The split here is set-based: product icons obey strokeWidth 1.5, brand marks are their own asset class outside the icon system.
- currentColor vs colored icons: inheriting color makes icons automatically theme-correct but cannot express multi-tone marks. Multi-tone needs fall to dedicated illustrations, not the icon set.
- Decorative default vs explicit-everything: defaulting to aria-hidden is safe for the common case and risks silence when someone builds a meaningful icon without a label. The mitigation is review discipline on icon-only patterns, where the rule inverts: label required.
Common Pitfalls & Failure Modes
1. Arbitrary pixel sizes
/* Bad */
<Icon icon={faStar} width={18} height={18} />
/* Good */
<Icon icon={faStar} width={20} height={20} /> /* icon.size.md */2. Labeling decorative icons
/* Bad — Noise: announced twice */
<button><Icon icon={faStar} label="Favorite" /> Favorite</button>
/* Good */
<button><Icon icon={faStar} /> Favorite</button>3. Unlabeled meaningful icons
/* Bad: A button with no accessible name */
<button><Icon icon={faXmark} /></button>
/* Good */
<button><Icon icon={faXmark} label="Close" /></button>4. Bypassing the component
/* Bad: Raw SVG skips the ARIA contract and hydration care */
<svg width="20"><path d="…" /></svg>
/* Good: One component owns the rules */
<Icon icon={faCheck} width={20} height={20} />5. Fixing alignment with margins
/* Bad: Per-instance optical correction */
.icon { margin-top: -2px; }
/* Good: Fix the glyph grid once; alignment comes free */Icon System Health Metrics
Signal 1: Size discipline
- Healthy: icon width/height props carry only the four scale steps (16/20/24/32); a grep for other numbers in icon sizing returns nothing.
- Warning: a fifth size circulates in one area—the 18px that "looked better next to 16px text"—and every future icon decision in that area now negotiates against it.
- Critical: arbitrary sizes are the norm; the scale is decorative, and cross-screen consistency is gone.
Signal 2: Labeling integrity
- Healthy: every icon-only control passes a label; every text-adjacent icon omits one—the binary is applied, not assumed.
- Warning: label audits pass on new components but legacy screens ship unlabeled icon-only buttons that everyone has stopped seeing.
- Critical: icon-only controls without accessible names—silent for screen readers, and a WCAG 4.1.2 defect with every click.
Signal 3: Component fidelity
- Healthy: product code renders icons only through the Icon component; raw
<svg>appears in the component itself and nowhere downstream. - Warning: a few inline SVGs bypass the ARIA branch—each one a place the labeling contract no longer applies.
- Critical: bypasses outnumber component usage; the contract is folklore now.
Migration Strategy: Wrangling an Icon Zoo
- Inventory the glyphs: every icon source (kit lookups, inline paths, sprite references), every size, every usage site. The dedupe is the payoff—most zoos run 3–4× duplicate concepts ("close" vs "x" vs "dismiss").
- Pick the canonical set: one glyph per concept, chosen on grid fit and stroke consistency; the losers map to the winners in a codemod table.
- Snap sizes to the scale (16/20/24/32) with the nearest-step tolerance—18px to 20 is +11%, inside the JND; nobody will notice, which is the point.
- Route through the component: replace inline SVGs with
<Icon>usages, labeling each by its new context's meaningful/decorative status—never migrating labels blindly. - Retire the zoo: delete unmapped glyphs; an icon nobody migrated to was decoration wearing a name.
Real-World Case Studies
Case 1: The toolbar that failed by ear
Five icon-only buttons passed visual review; the VoiceOver pass announced "button… button… button…"—the labels were never passed because the API made silence the default and nobody was listening. The fix was labels for all five; the prevention was the review rule: icon-only patterns are verified auditorily, always.
Case 2: The three closes
An audit found three distinct close glyphs (x, chevron, outline-x) at three sizes (18, 20, 22) across the product. The canonical-set migration collapsed them to one glyph at two sizes; the codemod table lives in the icon documentation so the fourth close never gets drawn.
Case 3: The icon that inherited color
A status icon was given its own fill token for a campaign—then broke in dark mode, then in the teal brand, then again inside a destructive button. Removing the fill and returning to currentColor made it correct everywhere at once: the context's semantics were always the right answer.
Verification Checklist
Additional Resources
- Elevation & Shadows — the other decorative-language system (
/blueprints/foundations/elevation) - Radius & Shape — shape as personality (
/blueprints/foundations/radius) - WCAG 1.1.1 non-text content — the normative basis for icon labeling (
https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html) - The sources —
ui/designTokens/core/icon.tokens.json,ui/components/Icon/Icon.tsxand itsIcon.contract.json
Related Concepts
Reflection Questions
Apply This Concept
Audit any toolbar in a product you use: which icons are meaningful, which are decorative, and what does a screen reader actually announce for each? Write the corrected label list.
Reflect
A team requests an 18px icon size because 20 looks too big next to 16px text. What questions decide whether the scale is wrong or the composition is wrong?
Reflect
Run the labeling-integrity signal on one screen you own: list every icon, its meaningful/decorative status, and what a screen reader actually announces. Which entry surprised you?
