Skip to main content
Version: 2.1.0
Last Updated: 2026-05-24
Scope: All modules. Sizes/colors: UI_UX_STANDARDS.md § Icons, SEMANTIC_COLORS.md.
Single reference for icon assignment, sizing, imports, and accessibility.

0. Direction A — Final Rules (canonical)

These rules are non-negotiable for all new and refactored UI. PR review enforces them. (The standalone audit:icons / audit:icon-approvals audit scripts were removed; the audit:appicon-tones audit still covers icon-tone conflicts.)
  1. Library: Lucide-react only. Render through <AppIcon> from @/shared/ui/icon. Raw <LucideIcon … /> JSX is allowed only inside leaf primitives we don’t own (e.g. shadcn shells); everywhere else use AppIcon.
  2. Stroke width: 1.5 (Apple-Minimal Outline). Never set strokeWidth inline on a Lucide tag — use <AppIcon> so the platform default applies, or pass weight="thin" | "regular" | "bold" for the rare exceptions (1.1 / 1.5 / 2.0).
  3. Size scale (locked, 4 tiers): xs/14, sm/16, md/20, lg/24. No other sizes. 2xs/12 exists only for legacy dense table chrome and must not be used in new code.
  4. Active-state behavior (navigation): the active item swaps the outline glyph for a filled variant — fill="currentColor" and strokeWidth={0} — paired with the text-primary tone. Use the getActiveIconSvgProps(isActive) helper in src/platform/navigation/utils/active-state.ts. Idle items stay outline at text-muted-foreground.
  5. Color tokens (allow-list): the only colors permitted on an icon are the semantic tones below — applied via the tone prop, never hex/text-gray-*/text-blue-* literals. Anything outside this list — including raw palette classes (text-blue-500, #0EA5E9) and per-module hex values — is rejected by the icon audit and PR review.
  6. Variants: variant="outline" (default) for the line look, or variant="solid" to fill the glyph with currentColor (zeroes the stroke). The active-state filled treatment in rule 4 is the canonical use of solid.
  7. Accessibility: decorative icons (default) get aria-hidden="true" automatically; meaningful icons must pass decorative={false} and a non-empty aria-label (TypeScript enforces this).

1. Quick Reference

Size tokens (Tailwind)

The scale is locked to four tiers. Do not introduce new sizes. Anything larger (32 / 48 px) is brand or illustration, not an icon — render the brand SVG (public/brand/*) or a dedicated illustration component, not a Lucide glyph.

AppIcon wrapper (preferred for shared/component code)

AppIcon from @/shared/ui/icon is the required way to render a Lucide icon in shared, platform, and core code. It enforces the locked stroke width, size scale, and color allow-list, and standardizes the decorative-vs-meaningful accessibility contract.
Raw <LucideIcon className="h-4 w-4" /> is no longer acceptable in new code. Existing leaf-primitive usages are tracked in reports/icon-audit-latest.json and must be migrated when the surrounding file is touched.

Import pattern

Accessibility

  • Icon-only buttons: aria-label="Action name".
  • Decorative (with visible text): aria-hidden="true" on the icon.

1.5 Approved Semantic Icons — Quick Reference

A scannable cheat sheet curated from §2 (universal/status/compliance), §5 (wizard step keys), §6 (module + healthcare), and §7 (anti-patterns). For full detail, jump to those sections.
Audit: The standalone audit:icons and audit:icon-approvals scripts were removed. Use npm run audit:appicon-tones to flag className-vs-tone conflicts (APPICON-001..004); raw Lucide / inline-stroke / hardcoded-color and deprecated-import drift are now caught in PR review against the rules in §0.

Approved icons by concept

Healthcare copy-paste templates: see examples/wizard-templates/healthcare-icon-keys.json for ready-to-use PF-41 snippets using stethoscope, pill, and syringe.
| Wizard steps | Personal info | User | user | PF-41 template key | | Wizard steps | Contact info | Mail | mail | PF-41 template key | | Wizard steps | Schedule | CalendarDays | calendar | PF-41 template key | | Wizard steps | Review & submit | ClipboardCheck | review | PF-41 template key | | Wizard steps | Complete | CheckCircle2 | complete | PF-41 template key | | Wizard steps | Unknown key | CircleHelp | (fallback) | Auto-rendered + dev console warning by resolveStepIcon() | | Module-specific | HR | Users | — | See §6 | | Module-specific | CL | Stethoscope | — | See §6 | | Module-specific | PM | ClipboardList | — | See §6 | | Module-specific | FA | DollarSign | — | See §6 | | Module-specific | RH | Home | — | Recovery housing (residential) | | Module-specific | GR | ShieldCheck | — | Governance & risk | | Module-specific | CE | Megaphone | — | Community engagement |

Preferred vs. deprecated variants

DO NOT CHANGE mappings

These mappings are locked across cores. Changing them creates cross-module visual drift or breaks template-driven wizards.

Quick example

Tone & variant (v1.3.0)

AppIcon accepts three additive props that eliminate hand-typed semantic color classes and stroke overrides. All are optional — omitting them is byte-identical to the legacy behavior. Resolution order on the rendered SVG (last wins): size class → tone class → caller className. So one-off escapes like className="text-amber-700" still override a tone.

Solid variant cookbook

variant="solid" flips the glyph from outlined to filled (sets fill="currentColor" and zeroes the stroke). It’s a visual weight choice, not a semantic one — tone still carries the meaning. Reach for solid only when the icon needs to read as a single dense anchor: a tinted badge chip, a status pip in dense tables, an avatar overlay, or an icon-in-circle hero on an empty/error state. Default to outline everywhere else (toolbars, nav, body copy, form affordances). Don’t use solid for:
  • Toolbar / icon-button affordances (Pencil, Trash2, Search, Filter) — outline is the default app feel.
  • Navigation rails and sidebar items — outline keeps the chrome quiet.
  • Decorative inline icons next to body text — solid will dominate the line.
  • Anything where the icon is larger than lg (24 px) — at hero sizes the filled shape can read as a logo.
Where this is already wired in shared components
  • ErrorState — hero icon uses variant="solid" inside the tinted circle wrapper (destructive and warning severities).
  • StatusBadge / createStatusBadge<T>() — accepts iconVariant: 'solid' | 'outline' per status key, so domain badges opt in to solid only for the states that warrant the extra weight.
For status-driven UIs, derive the tone from the registry instead of hard-coding it:

Domain status aliases (v1.4.0)

Many UIs render statuses that aren’t one of the six core SemanticStatus values — workflow lifecycle (draft, in_progress, completed), approvals (approved, pending, rejected), scheduling (confirmed, cancelled, no_show), billing (paid, overdue, void), and clinical (discharged, prospect). These are all aliased to existing AppIconTones via DOMAIN_STATUS_TONEno new status tokens, and no status-specific tone expansion. Use domainStatusToIconTone() so the same DB enum string drives the icon tone consistently across modules.
Notes:
  • Input is normalized (PascalCase, kebab-case, Title Case all work), so DB enums can be passed verbatim.
  • Unknown keys fall back to tone="muted" — the UI never breaks on a new/unmapped status, mirroring the wizard-step icon fallback (§5).
  • To add a new status, edit DOMAIN_STATUS_TONE and this table only; do not introduce a new AppIconTone value.

Cross-references

  • Full universal/status/compliance tables → §2 (Assignment Principles).
  • Wizard step string keys + resolveStepIcon() contract → §5.
  • Module + healthcare icon catalog → §6.
  • Anti-patterns and deprecations → §7.

2. Assignment Principles

  • Semantic consistency: Same concept → same icon across modules.
  • Visual differentiation: Similar but distinct concepts → different icons.
  • Accessibility: Sufficient contrast; pair with text labels.

Universal icons (DO NOT CHANGE)

Avoid: CheckCircle (deprecated) — use CheckCircle2. Scheduling: Use CalendarDays for scheduling; Calendar/CalendarIcon for date pickers.

Alert & status (do not interchange)

Compliance & security


3. Size, color, and usage patterns

Authority: Scale in src/index.css (--icon-xs--icon-lg); UI_UX_STANDARDS.md § Icons.
Color: Use semantic tokens (text-success, text-warning, text-destructive, text-muted-foreground). Do not use text-blue-500, text-green-600, etc.

4. Accessibility

  • Icon-only buttons: Always aria-label (e.g. aria-label="View dashboard").
  • Decorative icon next to text: aria-hidden="true" on the icon.
  • See UI_UX_STANDARDS.md § Accessibility for focus and ARIA.
Example:

5. Wizard step icons

Wizard steps use icons inside step circle indicators for visual orientation. The platform provides a curated catalog at src/platform/wizards/utils/stepIcons.ts. Two usage modes: Common step icon assignments: Sizing: size-4 (16px) inside step circles. Step circles are size-9 (desktop) / size-11 (mobile). See: WIZARD_DEVELOPMENT_GUIDE.md § Step Icons and WIZARD_UX_STANDARD.md § Step icons.

6. Module-specific patterns (summary)

Healthcare & clinical (CL / PM)


7. Anti-patterns

  • Do not use AlertTriangle for everything; differentiate by context.
  • Do not use TrendingDown for negative performance; use AlertOctagon for PIPs.
  • Do not use Shield for compliance; use ShieldCheck.
  • Do not use icons without labels for interactive elements.
  • Do not import * as Icons from "lucide-react"; import only what you use.
Deprecated: CheckCircleCheckCircle2; AlertCircleAlertTriangle or CircleAlert; HomeLayoutDashboard.

8. Adding new icons

  1. Search Lucide icons.
  2. Check if a similar concept exists in this guide.
  3. Prefer semantic icons; document the new icon here before use.

References

Lint enforcement

AppIcon usages are guarded by scripts/audit/audit-appicon-tones.ts, wired into validate:governance. Four rules keep tone as the single source of truth for icon color:
  • APPICON-001 (failure) — className carries a semantic color token (e.g. text-success) and tone is set. Pick one.
  • APPICON-002 (failure) — className carries a Tailwind palette class (e.g. text-amber-700). Use tone or a semantic token; palette classes don’t react to dark mode or PF-95 tenant theming.
  • APPICON-003 (failure) — tone literal isn’t a member of AppIconTone (typos like tone="successful").
  • APPICON-004 (warning) — variant="solid" paired with weight=… (weight is ignored when solid forces stroke-width 0).
Run locally: npm run audit:appicon-tones (human table) or npm run audit:appicon-tones -- --json (writes reports/audit-appicon-tones.json). Dynamic tone={…} / className={cn(...)} expressions are skipped with an info log, never failed.

Autofix

Most APPICON-001/002/004 violations are mechanical. Run npm run audit:appicon-tones:fix to rewrite them in place: dropping duplicate semantic className tokens (Fix A), converting standalone text-success/text-muted-foreground/etc. classes into the matching tone shorthand (Fix B), and removing dead weight props when variant="solid" is set (Fix D). Use npm run audit:appicon-tones:fix -- --dry-run to preview rewrites in reports/audit-appicon-tones-fix-preview.json without touching files. npm run audit:appicon-tones:fix -- --aggressive additionally maps Tailwind palette classes (text-amber-700tone="warning", etc.) to the closest semantic tone (Fix C). Palette → tone mapping is judgment-laden — always diff-review before committing. APPICON-003 typos (e.g. tone="successful") and dynamic className={cn(...)} expressions are never autofixed; the script reports them for manual fix.

9. Recent changes (PR-2, PR-3 — 2026-05-24)

This section captures the icon + token normalization landed in the icon-audit follow-up PRs. New code MUST follow these rules; touch-and-go migrations on existing files are tracked in reports/icon-audit-latest.json.

PR-2 — Color token normalization

What changed in src/index.css:
  • Module color aliases collapsed to canonical short codes. The duplicate --module-clinical, --module-finance, --module-operations, and --module-people aliases (and their @theme mappings) were removed. The full set of supported module identity tokens is now: --module-pf, --module-cl, --module-pm, --module-fm, --module-rh, --module-it, --module-hr, --module-lo, --module-ce, --module-fa, --module-gr, --module-fw, --module-th, --module-qo, --module-da, --module-rpt. Each token resolves to a single canonical HSL triplet per mode with paired *-foreground and *-subtle (background tint) variants.
  • Tint balance pass. Finance amber was desaturated so it sits in-band with the rest of the family (no longer the loudest tile). Subtle backgrounds were unified at L:92–94% in light mode and L:20–22% in dark mode so module tiles read as one set on both surfaces.
New rules (enforced by npm run audit:appicon-tones + PR review):
  1. Never use a long-form alias. text-[hsl(var(--module-clinical))] etc. are gone — use the short code (--module-cl) or, preferably, a semantic tone on AppIcon.
  2. Module identity tokens are nav/widget chrome only, never data series. Series colors stay on the chart palette (hsl(var(--chart-N)), N = 1–8) per SEMANTIC_COLORS.md.
  3. No inline style={{ color: tintColor }} on icons. Module tinting must flow through CSS custom properties on the surrounding tile, with the icon inheriting via tone="current" or tone="onAccent".

PR-3 — AppIcon migration (raw Lucide → <AppIcon>)

PR-3 mechanically rewrote ~140 raw <Icon className="size-N" /> call sites across navigation, dashboard widgets, wizards, and detail pages (see “Files changed” in the PR-3 commit) to use <AppIcon>. Going forward: Migration rules:
  1. No raw <LucideIcon … /> in src/cores/**, src/platform/**, or src/shared/components/**. Render through <AppIcon> from @/shared/ui/icon. The only exception is leaf primitives we don’t own (shadcn shells) — those are tracked in reports/icon-audit-latest.json and must migrate when touched.
  2. No className="size-N" / h-N w-N" for sizing an icon. Use size="xs" | "sm" | "md" | "lg" (14 / 16 / 20 / 24 px). The four-tier scale is locked.
  3. No inline strokeWidth={…}. The platform default is 1.5. For the rare exceptions use weight="thin" | "regular" | "bold".
  4. No palette/hex colors on icons. Use the tone allow-list (§0, rule 5). Status-driven UIs derive tone via semanticStatusToIconTone() or domainStatusToIconTone().
  5. Active-nav fill treatment goes through getActiveIconSvgProps(isActive) from src/platform/navigation/utils/active-state.ts, not hand-typed fill/strokeWidth props.
Step-by-step migration recipe:
For meaningful (non-decorative) icons, TypeScript will fail the build if you forget the label:
Verification after migrating a file:
The standalone audit:icons / audit:icon-approvals scripts were removed; raw Lucide / inline-stroke / hex offenders and deprecated-import drift are caught in PR review against §0. The remaining audit:appicon-tones audit can be run on demand.