Overview
Use semantic color tokens from the design system (src/index.css with Tailwind v4 @theme inline mappings); avoid hardcoded Tailwind palette classes (green-*, red-*, etc.) except where documented exceptions apply.
Canonical token governance and sync workflow are defined in DESIGN_TOKEN_SYNC_POLICY.md. When this document and runtime values diverge, follow the canonical-source and synchronization process in that policy.
What changed in v1.4.1 (2026-04-18): Chart Palette § “Colorblind safety” updated with the first formal audit results fromnpm run audit:chart-palette-colorblind(Brettel-Viénot-Mollon simulation + CIE76 ΔE in CIELAB). Audit reveals 9 fail / 25 warn pairs across both modes × 3 dichromacy profiles — see the new Known limitations & action items subsection. The mandatory rule “always pair color with shape/pattern/label” is now load-bearing for accessibility, not just defensive practice. What changed in v1.4.0 (2026-04-18): Added the Token Catalog Reference (full surface/role/status/special/sidebar inventory), a Module Identity Tokens section (12 active short codes + reserved + deprecated aliases), an expanded Chart Palette section, a Tenant Theming (PF-95) Mapping Table that documents the gaps between platform tokens and tenant-customizable colors, and a Compliance Theming (PF-91-EN-01) Mapping section. Includes a regression note for the previousinfo.fulldark-mode WCAG defect (now fixed).
Typography Token Contract
Typography parity follows the canonical design-system contract and app runtime mapping:- Canonical tokens:
--font-display: Space Grotesk--font-body: Inter--font-mono: DM Mono (code, monospace data)
- Runtime mappings:
font-sans→var(--tenant-font-family, var(--font-body))font-heading→var(--tenant-font-heading, var(--font-display))
--tenant-font-familyoverrides body/sans--tenant-font-headingoverrides heading/display
Standard Mappings
Token note (PF-95 update, 2026-04):--color-secondarynow maps tohsl(var(--secondary))(shadcn-canonical) so the tenant bridge--tenant-secondary-hslactually drivesbg-secondary/text-secondary. For the elevated card surface, usebg-card-elevateddirectly.--color-inputresolves to--input, which in:rootshares--tenant-border-hslwith--borderper shadcn convention — set the tenant border once and inputs follow. See DESIGN_TOKEN_SYNC_POLICY.md.
Badge Variants
Status-style badges must use Badge variants or StatusBadge (with statusConfig) from@/shared/components; do not use raw color classes. Use these Badge component variants from @/shared/ui/badge:
Note: Badge
info uses bg-info/10 text-info — a distinct informational blue. For primary-branded soft emphasis, use Button variant soft or primary-light Badge variant instead.
Subtle / Light Badge Variants
For low-emphasis or inline status (e.g. inside tables, alongside solid badges), use the*-light Badge variants from @/shared/ui/badge:
Use solid variants (
success, warning, destructive, info) for standalone status badges (cards, headers, filters). Use -light when the badge should not compete visually with surrounding content.
Specialty Badge Variants
Additional Badge variants exist for non-status use cases:
These are not for status indication — use the semantic/light variants above for statuses.
Status Badge Pattern
For domain statuses (e.g. invoice status, referral status, run status), map each status to a Badge variant and label, then render with the shared Badge or the shared StatusBadge component:- Define a config:
statusConfig: Record<Status, { label: string; variant: BadgeVariant; icon?: LucideIcon }>. - Render:
<Badge variant={config.variant}>{config.label}</Badge>or use<StatusBadge status={status} statusConfig={statusConfig} />from@/shared/components/StatusBadge. - Variant mapping: Use the canonical status-to-variant map in
@/shared/lib/status-variants:CANONICAL_STATUS_VARIANTSandgetCanonicalVariant(status). New status badges SHOULD use these canonical mappings; existing badges will be migrated incrementally. Fallback semantic guidance:success(active, approved, completed),warning(pending, in progress, expiring),destructive(rejected, error, cancelled, failed),info(new, submitted),secondary(draft, inactive, closed).
src/shared/components/StatusBadge.tsx for the shared component API and src/shared/lib/status-variants.ts for the canonical mapping.
When to Use Semantic vs Categorical Colors
Use Semantic Colors For:
- ✅ Status indicators (active, pending, error, complete)
- ✅ Validation feedback (valid, invalid, warning)
- ✅ Health indicators (healthy, at-risk, critical)
- ✅ Action feedback (success, failure)
- ✅ Priority levels (critical, high, medium, low)
Keep Categorical Colors For:
- 🎨 Data type indicators (string=blue, number=green, boolean=purple)
- 🎨 Core/module identifiers (hr=blue, rh=green, fa=amber)
- 🎨 Category distinctions that don’t imply status
- 🎨 Brand colors that must remain consistent
Utility Functions
Import from@/shared/lib/semantic-colors:
Examples
Before (DON’T DO THIS)
After (DO THIS)
Status Configuration Pattern
Icon Colors
Alert/Notification Styling
Progress/Score Indicators
Dark Mode Considerations
When using semantic tokens, dark mode is handled automatically:text-success→ Works in both light and dark modebg-success/10→ Proper opacity in both modesborder-success/20→ Consistent borders
Common Patterns Reference
Chart Colors
Charts (Recharts, etc.) should use the chart palette for data series so they respect light/dark theme and stay consistent.- CSS variables:
--chart-1through--chart-8are defined insrc/index.css(light and dark). - Tailwind:
chart.1…chart.8are mapped insrc/index.cssvia Tailwind v4@theme inline; usetext-chart-1,bg-chart-2, etc. for UI elements that reference chart colors (e.g. legend labels). - In chart config: Use
hsl(var(--chart-1)), …hsl(var(--chart-8))for strokes/fills, or pass aChartContainerconfig withtheme: { light: 'hsl(var(--chart-n))', dark: '...' }so series colors are theme-aware. - Do not use arbitrary hex or raw HSL in chart configs; use the design tokens above so charts adapt to dark mode.
Audit Checklist
Before committing, verify:- No direct Tailwind colors (
green-*,red-*,yellow-*, etc.) - All status indicators use Badge variants
- Icon colors use semantic tokens
- Alert/notification backgrounds use semantic tokens
- Progress indicators use semantic colors
- Light/dark mode both work correctly
Domain-Specific Status Mappings
Document Statuses
Credential Statuses
Workflow Statuses
Form Statuses
Real-World Usage Examples
Employee Status Card
Form Validation Messages
Dashboard Metrics
Alert Notifications
Table Row Status
Token Catalog Reference
The canonical token list lives insrc/index.css (:root + .dark declare HSL values; the @theme inline block exposes them as Tailwind v4 utilities). Use this section to look up which token to reach for; consult src/index.css for the exact HSL values.
Surfaces & roles (chrome)
Brand & action
Status
Use the Tailwind utilities
bg-success, bg-success-subtle, text-success, text-success-foreground, border-success/20, etc. The *-subtle tokens are pre-tinted backgrounds calibrated for both light and dark mode — prefer them over bg-{token}/10 when you want a consistent, mode-aware tint.
Regression note: Prior to 2026-04-18,semanticColorClasses.info.fullpairedbg-infowithtext-primary-foreground. In dark mode--infoflips to a light blue and--primary-foregroundis also light → light-on-light, WCAG AA fail. Now usestext-info-foreground. Cover with the Vitest intests/unit/shared/lib/semantic-colors.test.ts.
Special & utility
Density, Elevation & Encore v2
The.theme-encore-v2 class is an additive opt-in layer for visual rhythm and depth. It does not rename existing tokens, introduce PF-95 schema fields, or hardcode tenant colors; color-bearing surfaces still resolve through the semantic/PF-95 bridge above.
Current pilot consumers:
StatCard and ListFilterBar. Prefer these tokens for new shared primitives instead of repeating p-4 sm:p-6, h-9, or bespoke shadow classes.
Sidebar slot
The platform sidebar has its own token slot so it can be themed independently of the main app shell.Encore brand (reserved)
Module Identity Tokens
Each domain core has a dedicated identity color used for navigation accents, dashboard widget headers, and badge tinting. Tokens follow the pattern--module-{short-code}, --module-{short-code}-hover, --module-{short-code}-subtle.
Active short codes
Reserved (do not use yet)
Do not consume these in product UI without DS owner approval. See DESIGN_TOKEN_SYNC_POLICY.md.
Deprecated legacy aliases
Recommended usage
- Sidebar / nav row accent: 4px left border using
border-l-4 border-module-{id}for the active module. Reinforces a “color = module” mental model across the 12-core ERP. - Dashboard widget header underline:
border-b border-module-{id}so each module’s hub feels visually anchored. - Module badges:
bg-module-{id}-subtle text-module-{id}for tags that identify which module owns an entity (e.g. cross-cutting reports list). - Do NOT use module colors for status/state — use semantic status tokens (
success,warning,destructive,info) for that.
Chart Palette
Charts (Recharts, SVG, custom) MUST use the chart token palette so series colors stay consistent across modules and adapt to dark mode.Tokens
Mandatory rules
- Always use
hsl(var(--chart-N))in chart configs (Rechartsstroke/fill, SVG<rect fill={...}>, etc.). Never hardcode hex literals or raw HSL strings — they break dark mode and tenant rebrand. - Use Tailwind utilities (
text-chart-1,bg-chart-2, …) for legend swatches, KPI tile accents, etc. - Cap series at 8 categories before introducing a “(other)” bucket. Beyond 8, sequential blending or repetition produces unreadable charts.
- Pair with
ChartContainerfrom shadcn-charts (src/shared/ui/chart.tsx) when possible; passtheme: { light: 'hsl(var(--chart-N))', dark: '...' }so series colors remain theme-aware.
Colorblind safety
Audit status: First formal audit completed 2026-04-18 vianpm run audit:chart-palette-colorblind. Method: Brettel-Viénot-Mollon (single-projection) simulation under deuteranopia, protanopia, and tritanopia, with pairwise CIE76 ΔE in CIELAB across all 28 category pairs.
Result: ⚠ The current palette is not safe for color-only category encoding for colorblind users. 9 fail pairs (ΔE < 10) and 25 warn pairs (ΔE 10–25) across both modes × 3 profiles. Full report: reports/audits/chart-palette-colorblind-audit.json.
Mandatory rule: Charts MUST pair the chart palette color with at least one of: a category label, a marker shape, a pattern fill, or a series-name annotation. Color is for recognition, not for category identity. Never rely on color alone to distinguish data series.
Known problem pairs
Action items (deferred to future palette-revision spec)
- Replace one of
chart-2/chart-6to break the tritanopia teal/emerald collapse (e.g. swapchart-6to a yellow-green or olive). - Replace one of
chart-4/chart-7to break the amber/orange collapse (e.g. swapchart-7to a brown or burgundy). - Replace one of
chart-1/chart-3to break the sky/violet collapse (e.g. swapchart-3to a magenta with more red than blue). - Re-run
npm run audit:chart-palette-colorblind --strictafter each swap; goal is 0 fail pairs before wiring strict mode intovalidate:governance.
Tenant Theming (PF-95) Mapping Table
PF-95 (Tenant White-Labeling) lets each org override a subset of platform colors. The mapping below documents which platform tokens are tenant-customizable today and which remain platform-controlled. The primary admin UI isBrandingSettingsPage (src/platform/theming/pages/BrandingSettingsPage.tsx), which writes to pf_tenant_themes; on load, TenantThemeProvider injects --tenant-* overrides at :root.
Gaps (not tenant-customizable today)
These tokens exist at runtime but are not exposed to tenant overrides. To customize they require a PF-95 v2 expansion (separate spec):What tenants always get for free
- Tenant primary / accent are mode-aware: the
--tenant-*override applies to both light and dark mode at:root; the platform handles dark-mode adaptation downstream. - Contrast validation:
validateThemeContrast(insrc/platform/theming/utils/contrast.ts) enforces WCAG 2.1 AA on text-on-background pairs and 3:1 large-text on accent/destructive at save time. - Single active theme per org is enforced by partial unique index + DB trigger.
Compliance Theming (PF-91-EN-01) Mapping
PF-91-EN-01 (White-Label Compliance Dashboards) propagates PF-95 tenant theming to compliance surfaces (/settings/compliance/*) and edge-function exports.
- Frontend:
useComplianceTheme()(src/platform/compliance/) reads the active tenant theme and injects--tenant-*CSS custom properties on the compliance page container.useComplianceContrastValidatorflags any tenant color that fails WCAG AA on the regulatory dashboard and shows a<ContrastFallbackIndicator>to admins. - Immutable text: Apply the
compliance-text-immutableclass (src/platform/compliance/compliance-immutable.css) to regulatory or legal text that must never re-skin to a tenant brand. The class forces platform-default colors via!important. - Edge function:
_shared/compliance-contrast.ts(Deno) exportsPLATFORM_DEFAULTS(HEX) which mirrorsDEFAULT_THEME_COLORS(HEX) insrc/platform/theming/types.ts. Thegenerate-compliance-evidenceedge function uses these for evidence-package metadata (branded: true/false,contrast_validated). - Parity: All three sources (
src/index.css,DEFAULT_THEME_COLORS,PLATFORM_DEFAULTS) MUST resolve to the same semantic palette. Thenpm run audit:token-parityscript (Phase 4 of the color-system review) will enforce this contract.
Audit Checklist (extended)
Before committing UI changes, verify (in addition to the original checklist above):- No raw hex in chart configs — use
hsl(var(--chart-N)) - New chart with categorical color encoding pairs each color with a label, shape, or pattern (per the colorblind audit findings above)
- Module colors use short-code tokens (
--module-cl), not legacy aliases (--module-clinical) - Tenant-customizable surfaces use semantic tokens — not encore brand or hex literals — so PF-95 overrides take effect
- If you introduce a new
--tenant-*consumer, add it to the PF-95 Mapping Table above - If you change any
--chart-Nvalue, re-runnpm run audit:chart-palette-colorblindand update the Known problem pairs table - If you add a new shadcn-canonical token, register it in
REQUIRED_TOKENSinscripts/audit/verify-shadcn-tokens.mjsAND define it under both:rootand.darkinsrc/index.css. Runnpm run audit:shadcn-tokensto confirm.
Token Coverage Gate
The scriptscripts/audit/verify-shadcn-tokens.mjs (npm: audit:shadcn-tokens,
chained into validate:governance) statically asserts that every
shadcn-canonical theming token is declared in both the :root and .dark
blocks of src/index.css. Drift fails the build with a grouped diff of which
tokens are missing in which block.
Audited tokens (33):
- Surface + foreground pairs:
background/foreground,card/card-foreground,popover/popover-foreground,primary/primary-foreground,secondary/secondary-foreground,muted/muted-foreground,accent/accent-foreground,destructive/destructive-foreground - Singles:
border,input,ring - Chart palette:
chart-1…chart-5 - Sidebar:
sidebar-background,sidebar-foreground,sidebar-primary,sidebar-primary-foreground,sidebar-accent,sidebar-accent-foreground,sidebar-border,sidebar-ring - Radius:
radius
success/warning/info and their *-foreground pairs,
--encore-*, --module-*, --tenant-*) are intentionally not covered by
this gate — they’re enforced by audit:token-parity and the PF-95 mapping
table above. To extend the gate, edit REQUIRED_TOKENS in the script and add
the matching declarations to src/index.css.
Related Documentation
constitution.md §6.7- UI/UX Guardrailsdocs/development/UI_UX_STANDARDS.md- Full UI/UX Standardsdocs/development/DESIGN_TOKEN_SYNC_POLICY.md- Token governance, parity contract, deprecated/reserved tokenssrc/index.css- Canonical token definitions (:root,.dark,@theme inline)src/shared/lib/semantic-colors.ts- Utility functionssrc/shared/ui/badge.tsx- Badge component with variantssrc/platform/theming/- PF-95 tenant theming (types, hooks, components)src/platform/compliance/- PF-91-EN-01 white-label compliance dashboardstests/unit/shared/lib/semantic-colors.test.ts- Regression tests for semantic class set