Dev view

Icon

An SVG-based pictogram rendered inline with surrounding text or as a standalone glyph inside another component. Carries either decorative or meaningful semantics depending on whether a visible text label accompanies it. The canonical primitive that every larger component composes for affordance, status, or wayfinding cues.

Also called SVG Glyph Pictogram

When to use

Use

For affordance cues (chevron in a disclosure trigger, search glyph in a search input), status cues (check, exclamation, info), wayfinding cues (arrow, home, settings), and brand / decorative imagery that pairs with text. Always SVG when possible — token-colour flows through `currentColor`, sizing is driven by canonical size-tokens, and the asset scales cleanly across DPR.

Avoid

For substantive image content (photographs, illustrations) — that is `<img>` with alt text rather than an icon glyph. For icon-only buttons that lack an accessible name — use `Button` with `aria-label`, not a bare icon. For decorative imagery that exceeds the size-token range — use a brand-image or hero- media slot in the host component.

Versus related

  • button

    `Button` is an interactive activator that may host an Icon (icon-leading, icon-trailing, or icon-only). `Icon` is the glyph itself, never interactive on its own. Icon-only buttons compose Button + Icon and carry `aria-label` on the button; bare icons in a clickable host are an anti-pattern.

  • link

    `Link` is the navigation primitive; it may host an Icon (external-link arrow, download glyph) as a decorative cue alongside the link text. The Icon contributes visual semantics; the Link's text carries the accessible name.

  • badge

    `Badge` is a metadata marker that may host an Icon (icon-leading slot) for severity glyphs (check, exclamation). The Icon is decorative when paired with the badge content; meaningful when the badge is dot- only.

  • avatar

    `Avatar` is the entity-representation primitive that hosts Icon as its `fallback-icon` slot — the ultimate fallback when neither image nor initials are available. The Icon represents a generic-person glyph; Avatar represents the specific entity. The accessible-name for Avatar lives on the container, not on the Icon.

Icon is the canonical SVG-glyph primitive — a pictogram that renders inline with text or as a standalone glyph inside a host component. The semantic split is decorative versus meaningful: paired with a visible label, the icon is decorative and hides from assistive tech; without a label, the icon is meaningful and carries an accessible name. Two size families (token-driven and inline-with- text), two semantic modes (decorative, meaningful), three colour channels (stroke, fill, currentColor inheritance). The reference documents the role-img versus aria-hidden contract, the currentColor inheritance pattern that lets token-colour flow through, and the canonical sizing tokens that keep icons in scale with their host.

Highlight
Fig 1.1 · Icon · Dev view
Dev

Code anatomy

Slot Code slot Semantic
root root svg
stroke stroke svg-path
fill fill svg-path
Both

Variants, properties, states

Variants

Structurally different versions of the component.

outline solid

Properties

The same component, parameterised.

PropertyType
size xs | sm | md | lg | xl
meaningful boolean
hasStroke boolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
data
idleinheritingexplicit-colorreduced-motion
Dev

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components A `<ui-icon>` host with `name` attribute selecting from a registry; renders an inline `<svg>` with `viewBox` from the registry entry; size attribute mapped to a CSS variable attributes (`name="search"`, `size="md"`, `meaningful`); presence of `meaningful` toggles `role="img"` and consumes `label` attribute as the accessible name
React Icon component imports from a registry (lucide-react, react-icons, custom set) and renders an inline `<svg>`; libraries include Heroicons, Phosphor Icons, Tabler Icons, Lucide props (`name`, `size`, `meaningful`); the `aria-label` prop pairs with `meaningful: true`; size prop maps to `width` and `height` on the svg root
Angular (signals) Angular component with `<ng-content>` projecting a chosen SVG, or input<IconName>() selecting from a registry; `[attr.role]` and `[attr.aria-hidden]` driven by signals input<IconName>(); input<'xs' | 'sm' | 'md' | 'lg' | 'xl'>(); input<boolean>('meaningful') toggling role
Vue Single-file component with named slots or registry-driven rendering; v-bind for size and aria attributes defineProps with literal-union types; conditional aria-hidden via computed prop based on meaningful flag
Both

Accessibility

Slot Accessibility hint
root Decorative icons set `aria-hidden="true"` (and no role). Meaningful icons carry `role="img"` plus `aria-label` describing the meaning ("Sort ascending", "Open menu"). Never both `aria-hidden="true"` and `role="img"` — they contradict.
stroke Vector paths inside an SVG are presentational by default. No role needed; the SVG root carries the accessible name via `aria-label` when meaningful.
fill Vector paths inside an SVG are presentational by default. No role needed.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
Tab (decorative icon)Decorative icons are not in the tab order. Tab moves through the host element; the icon is presentational and does not receive focus.
Tab (icon hosted inside a focusable element)Focus enters the host (button, link, list-item). The icon does not separately participate; the host carries the focus ring.

Screen-reader announcements

TriggerExpected
SR encounters a decorative iconSilent. `aria-hidden="true"` excludes the icon from the accessibility tree; the host's accessible name reads without icon-name interference.
SR encounters a meaningful iconSR announces the `aria-label` value, prefixed by the role announcement ("graphic, Open menu" or "image, Verified"). The label describes the icon's meaning, not the glyph shape.

axe-core rules to assert

  • svg-img-alt
  • role-img-alt
  • aria-allowed-attr
  • aria-hidden-focus
  • color-contrast

Same data as JSON for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe: /api/components/icon/a11y-fixture.json

Both

Contracts

Non-negotiable contracts

  1. APGWAI-ARIA aria-hidden + role=img patterns

    Decorative icons set `aria-hidden="true"` and no role. Meaningful icons set `role="img"` plus `aria-label` and no `aria-hidden`. Never both — the combination is a contradiction.

    Without the binary distinction, browsers and SR resolve the combined attributes inconsistently. Some surfaces announce the label twice (once via role=img, once via the host); others ignore the icon entirely. The contract gives every consumer a deterministic announcement.

  2. HTML specHTML inline SVG with currentColor keyword (W3C SVG + CSS Color)

    SVG `fill` and `stroke` are bound to `currentColor` so token-colour flows through CSS `color` inheritance. Hard-coded hex values in the SVG are reserved for brand-glyph cases documented at the host level.

    Hard-coded hex breaks theming and forces icon-by-icon edits when the palette shifts. `currentColor` is the cheapest way to keep icons in step with their host text across light / dark / brand-swap themes without per-icon overrides.

  3. Canon

    Icon-only buttons / links / badges (no visible label) carry the accessible name on the host element via `aria-label`, never on the icon itself. The icon stays decorative; the host carries the meaning.

    Putting `aria-label` on the icon when it sits inside a button creates a name-from-content collision — SR-output may include both labels in unpredictable order. The host is the activator; the host owns the name.

Vocabulary drift

HTML
svg / img
Native `<svg>` is the canonical icon-shape for inline glyphs; `<img>` with an SVG src is acceptable for non-inheriting icons but loses `currentColor` and token-driven sizing.
Material 3
Icon (Material Symbols)
Material Symbols ships variable-axis icons (weight, grade, optical-size) on top of the canonical contract. Implementation audits document the per-axis mapping.
Polaris
Icon
Carbon
Icon
Atlassian
Icon
Dev

Common mistakes

Blocker

#icon-meaningful-no-label

Meaningful icon without an accessible name

Problem

The icon carries semantic meaning (lock, warning, error, external) but has no `role="img"` and no `aria-label`. Sighted users see the glyph; SR users encounter nothing where the meaning lives.

Fix

Set `role="img"` plus `aria-label` describing the icon's meaning. Never label the glyph itself ("checkmark", "exclamation-mark") — label the meaning ("Verified", "Warning"). When the icon is the entire content of a larger element (icon-only button, dot badge), move the accessible name to the host instead.

Blocker

#icon-conflicting-aria-roles

Both `aria-hidden="true"` and `role="img"` set

Problem

The icon carries both `aria-hidden="true"` (says: ignore me) and `role="img"` plus `aria-label` (says: read this label). Browsers and SR resolve the contradiction inconsistently; the canonical semantic is undefined.

Fix

Pick one. Decorative icons set `aria-hidden="true"` and no role / label. Meaningful icons set `role="img"` plus `aria-label` and no `aria-hidden`. Lint the component to reject the combination.

Major

#icon-decorative-not-hidden

Decorative icon without `aria-hidden`

Problem

A decorative icon paired with a visible label (button + icon-leading, link + external-arrow, badge + severity glyph) lacks `aria-hidden="true"`. SR reads "checkmark Save" or "exclamation-mark Warning" — verbose echo of the visible label, no added information.

Fix

Set `aria-hidden="true"` on every decorative icon. The visible label carries the accessible name; the icon is visual reinforcement only. Document the decorative-vs- meaningful distinction in the host component's a11y hints so authors do not flip the flag inconsistently.

Major

#icon-color-not-currentcolor

Icon colour hard-coded

Problem

The icon's `fill` or `stroke` is set to a fixed hex or named colour. Theming (light / dark, brand swap) breaks because the icon stays its hard-coded hue while the surrounding text shifts. The icon visually disconnects from its host.

Fix

Use `fill="currentColor"` (filled icons) or `stroke="currentColor"` (outline icons) in the SVG. The colour inherits from the CSS `color` property of the host, following theme automatically. Explicit per-instance colour overrides go through a documented `color` prop or a host-level CSS variable, not through hard-coded hex values inside the SVG.

Figma↔Code mismatches
  1. 01
    Figma

    A specific icon variant (e.g. "Search-outline-md") drawn as a separate component

    Code

    A single Icon component with a `name` prop selecting from an icon registry

    Consequence

    Designers ship N × M Figma components (every icon × every size); developers ship one Icon component with a name and a size prop. The Figma roster grows linearly with the icon set; the code roster stays at one. Drift is invisible because the Figma file does not enumerate "missing icons" against the code registry.

    Correct

    Model Icon as a single component with `name` and `size` props in both Figma and code. The Figma component carries a "name" component property bound to the icon-set token list; the code Icon imports from the same registry.

  2. 02
    Figma

    Icon size hard-coded as a px value

    Code

    Size driven by a token (`text.md`, or icon-specific tokens like `--ui-icon-md`)

    Consequence

    Designers set `width: 16px` directly on the icon frame; developers bind size to a token. When the type-scale rebalances, the Figma file lags and the shipped icon visually disagrees with the surrounding text size.

    Correct

    Bind size to a token in both surfaces. Figma uses a "size" component property mapped to `xs / sm / md / lg / xl`; code uses the same enum. Icon size matches the host text size by default and falls back to explicit per-host overrides only when documented.

  3. 03
    Figma

    Icon colour drawn as a fixed hex

    Code

    Colour driven by `currentColor` inheriting from the host text colour

    Consequence

    Designers fix a black or grey hue in the Figma frame; developers ship `fill="currentColor"` so the colour inherits from the host. The two diverge any time the host text colour is not the default; theming (light / dark) breaks the design-fidelity claim.

    Correct

    Use `currentColor` for fill and stroke in both Figma icon-set tokens and code. The icon colour follows the host text colour automatically; explicit colour overrides go through a documented `color` prop, not by hard-coding a hex.

  4. 04
    Figma

    Meaningful icon drawn without an accompanying label

    Code

    A bare `<svg>` with no `role="img"` and no `aria-label`

    Consequence

    Designers draw an icon that genuinely carries meaning (a "lock" indicating account locked, a "warning" indicating a risk) but provide no label. Developers ship the SVG as decorative; SR users encounter the locked / warning glyph without any AT signal.

    Correct

    Distinguish meaningful from decorative at design time. The Figma component property "meaningful" toggles the accessible-name field. Code reads the prop to either set `aria-hidden="true"` (decorative) or `role="img"` plus `aria-label` (meaningful). The two surfaces share the same flag.