Skip to main content
Version: 1.4.1 Last Updated: 2026-04-18 Status: Active Target Audience: Developers

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 from npm 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 previous info.full dark-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-sansvar(--tenant-font-family, var(--font-body))
    • font-headingvar(--tenant-font-heading, var(--font-display))
Tenant-specific typography remains supported via PF-95 variables:
  • --tenant-font-family overrides body/sans
  • --tenant-font-heading overrides heading/display

Standard Mappings

Token note (PF-95 update, 2026-04): --color-secondary now maps to hsl(var(--secondary)) (shadcn-canonical) so the tenant bridge --tenant-secondary-hsl actually drives bg-secondary / text-secondary. For the elevated card surface, use bg-card-elevated directly. --color-input resolves to --input, which in :root shares --tenant-border-hsl with --border per 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:
  1. Define a config: statusConfig: Record<Status, { label: string; variant: BadgeVariant; icon?: LucideIcon }>.
  2. Render: <Badge variant={config.variant}>{config.label}</Badge> or use <StatusBadge status={status} statusConfig={statusConfig} /> from @/shared/components/StatusBadge.
  3. Variant mapping: Use the canonical status-to-variant map in @/shared/lib/status-variants: CANONICAL_STATUS_VARIANTS and getCanonicalVariant(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).
See 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 mode
  • bg-success/10 → Proper opacity in both modes
  • border-success/20 → Consistent borders
Avoid manual dark mode overrides like:

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-1 through --chart-8 are defined in src/index.css (light and dark).
  • Tailwind: chart.1chart.8 are mapped in src/index.css via Tailwind v4 @theme inline; use text-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 a ChartContainer config with theme: { 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 in src/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.full paired bg-info with text-primary-foreground. In dark mode --info flips to a light blue and --primary-foreground is also light → light-on-light, WCAG AA fail. Now uses text-info-foreground. Cover with the Vitest in tests/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. 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

  • 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 (Recharts stroke / 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 ChartContainer from shadcn-charts (src/shared/ui/chart.tsx) when possible; pass theme: { light: 'hsl(var(--chart-N))', dark: '...' } so series colors remain theme-aware.

Colorblind safety

Audit status: First formal audit completed 2026-04-18 via npm 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-6 to break the tritanopia teal/emerald collapse (e.g. swap chart-6 to a yellow-green or olive).
  • Replace one of chart-4 / chart-7 to break the amber/orange collapse (e.g. swap chart-7 to a brown or burgundy).
  • Replace one of chart-1 / chart-3 to break the sky/violet collapse (e.g. swap chart-3 to a magenta with more red than blue).
  • Re-run npm run audit:chart-palette-colorblind --strict after each swap; goal is 0 fail pairs before wiring strict mode into validate:governance.
Why ship the audit before fixing the palette? The current palette is in production and changing series colors retrospectively would re-skin every chart in the platform. The audit + mandatory pairing rule mitigates the risk today; the swap is a coordinated visual-regression PR that needs DS owner sign-off.

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 is BrandingSettingsPage (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 (in src/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. useComplianceContrastValidator flags any tenant color that fails WCAG AA on the regulatory dashboard and shows a <ContrastFallbackIndicator> to admins.
  • Immutable text: Apply the compliance-text-immutable class (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) exports PLATFORM_DEFAULTS (HEX) which mirrors DEFAULT_THEME_COLORS (HEX) in src/platform/theming/types.ts. The generate-compliance-evidence edge 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. The npm run audit:token-parity script (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-N value, re-run npm run audit:chart-palette-colorblind and update the Known problem pairs table
  • If you add a new shadcn-canonical token, register it in REQUIRED_TOKENS in scripts/audit/verify-shadcn-tokens.mjs AND define it under both :root and .dark in src/index.css. Run npm run audit:shadcn-tokens to confirm.

Token Coverage Gate

The script scripts/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-1chart-5
  • Sidebar: sidebar-background, sidebar-foreground, sidebar-primary, sidebar-primary-foreground, sidebar-accent, sidebar-accent-foreground, sidebar-border, sidebar-ring
  • Radius: radius
Project extensions (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.
  • constitution.md §6.7 - UI/UX Guardrails
  • docs/development/UI_UX_STANDARDS.md - Full UI/UX Standards
  • docs/development/DESIGN_TOKEN_SYNC_POLICY.md - Token governance, parity contract, deprecated/reserved tokens
  • src/index.css - Canonical token definitions (:root, .dark, @theme inline)
  • src/shared/lib/semantic-colors.ts - Utility functions
  • src/shared/ui/badge.tsx - Badge component with variants
  • src/platform/theming/ - PF-95 tenant theming (types, hooks, components)
  • src/platform/compliance/ - PF-91-EN-01 white-label compliance dashboards
  • tests/unit/shared/lib/semantic-colors.test.ts - Regression tests for semantic class set