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 standaloneaudit:icons / audit:icon-approvals audit scripts were
removed; the audit:appicon-tones audit still covers icon-tone conflicts.)
-
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 useAppIcon. -
Stroke width:
1.5(Apple-Minimal Outline). Never setstrokeWidthinline on a Lucide tag — use<AppIcon>so the platform default applies, or passweight="thin" | "regular" | "bold"for the rare exceptions (1.1/1.5/2.0). -
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. -
Active-state behavior (navigation): the active item swaps the
outline glyph for a filled variant —
fill="currentColor"andstrokeWidth={0}— paired with thetext-primarytone. Use thegetActiveIconSvgProps(isActive)helper insrc/platform/navigation/utils/active-state.ts. Idle items stay outline attext-muted-foreground. -
Color tokens (allow-list): the only colors permitted on an icon
are the semantic tones below — applied via the
toneprop, 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. -
Variants:
variant="outline"(default) for the line look, orvariant="solid"to fill the glyph withcurrentColor(zeroes the stroke). The active-state filled treatment in rule 4 is the canonical use ofsolid. -
Accessibility: decorative icons (default) get
aria-hidden="true"automatically; meaningful icons must passdecorative={false}and a non-emptyaria-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 standaloneaudit:iconsandaudit:icon-approvalsscripts were removed. Usenpm run audit:appicon-tonesto 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| Wizard steps | Personal info |examples/wizard-templates/healthcare-icon-keys.jsonfor ready-to-use PF-41 snippets usingstethoscope,pill, andsyringe.
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.
ErrorState— hero icon usesvariant="solid"inside the tinted circle wrapper (destructiveandwarningseverities).StatusBadge/createStatusBadge<T>()— acceptsiconVariant: 'solid' | 'outline'per status key, so domain badges opt in to solid only for the states that warrant the extra weight.
Domain status aliases (v1.4.0)
Many UIs render statuses that aren’t one of the six coreSemanticStatus 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_TONE — no 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 Caseall 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_TONEand this table only; do not introduce a newAppIconTonevalue.
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 insrc/index.css (--icon-xs … --icon-lg); UI_UX_STANDARDS.md § Icons.
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.
5. Wizard step icons
Wizard steps use icons inside step circle indicators for visual orientation. The platform provides a curated catalog atsrc/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
AlertTrianglefor everything; differentiate by context. - Do not use
TrendingDownfor negative performance; useAlertOctagonfor PIPs. - Do not use
Shieldfor compliance; useShieldCheck. - Do not use icons without labels for interactive elements.
- Do not
import * as Icons from "lucide-react"; import only what you use.
CheckCircle → CheckCircle2; AlertCircle → AlertTriangle or CircleAlert; Home → LayoutDashboard.
8. Adding new icons
- Search Lucide icons.
- Check if a similar concept exists in this guide.
- 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) —
classNamecarries a semantic color token (e.g.text-success) andtoneis set. Pick one. - APPICON-002 (failure) —
classNamecarries a Tailwind palette class (e.g.text-amber-700). Usetoneor a semantic token; palette classes don’t react to dark mode or PF-95 tenant theming. - APPICON-003 (failure) —
toneliteral isn’t a member ofAppIconTone(typos liketone="successful"). - APPICON-004 (warning) —
variant="solid"paired withweight=…(weight is ignored when solid forces stroke-width 0).
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. Runnpm 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-700 → tone="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 inreports/icon-audit-latest.json.
PR-2 — Color token normalization
What changed insrc/index.css:
-
Module color aliases collapsed to canonical short codes. The
duplicate
--module-clinical,--module-finance,--module-operations, and--module-peoplealiases (and their@thememappings) 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*-foregroundand*-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.
npm run audit:appicon-tones + PR review):
- Never use a long-form alias.
text-[hsl(var(--module-clinical))]etc. are gone — use the short code (--module-cl) or, preferably, a semantictoneonAppIcon. - 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) perSEMANTIC_COLORS.md. - No inline
style={{ color: tintColor }}on icons. Module tinting must flow through CSS custom properties on the surrounding tile, with the icon inheriting viatone="current"ortone="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:
- No raw
<LucideIcon … />insrc/cores/**,src/platform/**, orsrc/shared/components/**. Render through<AppIcon>from@/shared/ui/icon. The only exception is leaf primitives we don’t own (shadcn shells) — those are tracked inreports/icon-audit-latest.jsonand must migrate when touched. - No
className="size-N"/h-N w-N"for sizing an icon. Usesize="xs" | "sm" | "md" | "lg"(14 / 16 / 20 / 24 px). The four-tier scale is locked. - No inline
strokeWidth={…}. The platform default is1.5. For the rare exceptions useweight="thin" | "regular" | "bold". - No palette/hex colors on icons. Use the
toneallow-list (§0, rule 5). Status-driven UIs derive tone viasemanticStatusToIconTone()ordomainStatusToIconTone(). - Active-nav fill treatment goes through
getActiveIconSvgProps(isActive)fromsrc/platform/navigation/utils/active-state.ts, not hand-typedfill/strokeWidthprops.
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.