Theming
Theme Matrix
Every theme is a combination of three dimensions:
| Dimension | Options |
|---|---|
| Mode |
|
| Contrast |
|
| Color vision |
|
This produces 24 theme variants. Token JSON files live in mode/{dark,light}/:
default.json → dark + default contrast + standard visiondimmer.json → dark + dimmer + standard vision (deprecated, hidden from product UI)high-contrast.json → dark + high-contrast + standard visiondefault-protanopia.json → dark + default + protanopiadimmer-deuteranopia.json → dark + dimmer + deuteranopiahigh-contrast-tritanopia.json → dark + high-contrast + tritanopia...Build Pipeline
cd packages/core-design-systempnpm build # → node scripts/build.jsThe build uses Style Dictionary + @tokens-studio/sd-transforms:
- Reads
$themes.jsonand permutates all theme combinations - Resolves token references (
{gray.500}→ actual LCH value) - Applies custom transforms:
alpha,darken,lightenin LCH space;dropShadowfor data-viz - Marks tokens as
themeableorcore(determines which output file they land in) - Outputs to
dist/:
dist/├── styles/│ ├── core-imports.css # @imports for all core files│ ├── colors.css # Core color primitives (:root)│ ├── core.css # Dimensions, typography (:root)│ ├── breakpoint.css # Layout tokens (:root)│ ├── components.css # Component tokens (:root)│ ├── themes.css # @imports for all 24 theme files│ ├── dark.css # Semantic tokens under .dark selector│ ├── light.css│ ├── dark-dimmer.css│ ├── ... # 24 theme files total│ └── mfe-themes.css # Subset for micro-frontend consumers│└── styles-esm/ ├── index.ts # Theme map + re-exports ├── colors.ts # Programmatic access to primitives └── ... # TypeScript module per fileAll CSS properties use the cn prefix: --cn-bg-0, --cn-text-1, --cn-set-brand-primary-bg, etc.
CSS Selectors
The build script maps each theme file to CSS class selectors in the format .{mode}-{color}-{contrast}:
| Token file | CSS selectors |
|---|---|
dark/default.json | .dark, .dark-std-std |
light/default.json | .light, .light-std-std |
dark/dimmer.json | .dark-dimmer, .dark-std-low |
light/high-contrast.json |
|
dark/default-protanopia.json |
|
light/dimmer-tritanopia.json |
|
To switch themes at runtime, set the class on <body>:
document.body.className = "dark-std-std";All CSS custom properties update instantly — no JS rerender needed for color changes.
Contrast Modes
Standard and High Contrast are the user-selectable contrast levels in product UI. Each level shifts shade steps and alpha values uniformly — no layout or component changes needed.
Standard (default)
The baseline. Optimized for typical displays in well-lit environments. Text-to-background ratios comfortably exceed WCAG AA across all semantic roles.
High Contrast
Amplifies separation between foreground and background for bright environments, small screens, or users who need stronger differentiation. Text shifts ~1 step darker (light mode) or brighter (dark mode), borders become more prominent, and interactive-state alphas increase.
Light mode
| Token | Standard | High Contrast |
|---|---|---|
bg.0 | gray.25 | gray.25 |
bg.1 | pure.white | pure.white |
bg.2 | gray.25 | gray.25 |
bg.3 | pure.white | pure.white |
text.1 | pure.black | pure.black |
text.2 | gray.850 | gray.850 |
text.3 | gray.700 | gray.800 |
text.4 | gray.600 | gray.700 |
border.1 | gray.500 | gray.600 |
border.2 | gray.150 | gray.300 |
border.3 | gray.100 | gray.200 |
border.success | lime.500 | lime.600 |
border.danger | red.500 | red.600 |
border.warning | yellow.400 | yellow.500 |
state.hover | gray.200 alpha 0.23 | gray.200 alpha 0.30 |
state.selected | gray.300 alpha 0.26 | gray.300 alpha 0.34 |
Dark mode
| Token | Standard | High Contrast |
|---|---|---|
bg.0 | pure.black | pure.black |
bg.1 | gray.1000 | gray.1000 |
bg.2 | gray.950 | gray.950 |
bg.3 | gray.950 | gray.950 |
text.1 | gray.25 | pure.white |
text.2 | gray.200 | gray.150 |
text.3 | gray.500 | gray.400 |
text.4 | gray.600 | gray.500 |
text.brand | blue.400 | blue.300 |
border.1 | gray.500 | gray.400 |
border.2 | gray.850 | gray.800 |
border.3 | gray.900 | gray.850 |
border.success | forest.500 | forest.400 |
border.danger | red.600 | red.500 |
border.warning | yellow.500 | yellow.400 |
state.hover | gray.850 alpha 0.20 | gray.850 alpha 0.36 |
state.selected | gray.850 alpha 0.25 | gray.850 alpha 0.40 |
Color Vision Adaptations
Color-blind modes remap semantic hues so that information is conveyed through brightness hierarchy rather than hue alone. The human retina has three cone types — L (red), M (green), and S (blue). When one type is absent, certain color pairs become indistinguishable. Our adaptations shift the affected hues to perceptually distinct alternatives.
Each CVD type is a separate theme mode (e.g. light-std-protanopia, dark-std-deuteranopia). The remapping is applied at the token level — components need no changes.
Protanopia (red-blind)
Missing L-cones (long wavelength). Red appears dark and muddy, indistinguishable from dark green or brown. Affects ~1.3% of males, ~0.02% of females. Because red is severely darkened, we shift danger to the brighter yellow and cascade other warm hues down. Brand (indigo) remains unchanged since blue-violet perception is intact.
| Role | Default | Adapted | Rationale | |
|---|---|---|---|---|
brand | indigo | — | indigo | Unchanged, blue-violet visible |
success | lime | → | indigo | Green invisible, shift to blue-violet |
danger | red | → | yellow | Red invisible, shift to bright warm |
warning | yellow | → | orange | Darker than danger for hierarchy |
set.blue | blue | → | cyan | Differentiate from brand (indigo) |
set.forest-green | forest | → | cyan | Green invisible, shift to cool blue |
set.mint | mint | → | indigo | Green-teal invisible |
set.orange | orange | → | brown | Freed for warning |
set.pink | pink | → | purple | Red component invisible |
Deuteranopia (green-blind)
Missing M-cones (medium wavelength). Green merges with red into a brownish-khaki range. Affects ~1.2% of males, ~0.01% of females. Similar confusion axis to protanopia, but reds are not darkened — only hue is lost. Brand shifts from indigo to blue for stronger separation from the success role (also remapped to indigo).
| Role | Default | Adapted | Rationale | |
|---|---|---|---|---|
brand | indigo | → | blue | Separate from success (indigo) |
success | lime | → | indigo | Green invisible, shift to blue-violet |
danger | red | → | yellow | Red merges with green, shift to warm |
warning | yellow | → | orange | Darker than danger for hierarchy |
set.forest-green | forest | → | blue | Green invisible, shift to blue |
set.mint | mint | → | indigo | Green-teal invisible |
set.orange | orange | → | brown | Freed for warning |
set.pink | pink | → | purple | Red component invisible |
Tritanopia (blue-yellow blind)
Missing S-cones (short wavelength). Blue merges with green, and yellow becomes indistinguishable from light pink. Very rare — affects ~0.003% of the population, equally across genders (not X-linked). Brand (indigo) stays unchanged since even without S-cones, the luminance channel still differentiates blue from other hues.
| Role | Default | Adapted | Rationale | |
|---|---|---|---|---|
warning | yellow | → | orange | Yellow invisible |
set.orange | orange | → | brown | Freed for warning |
set.purple | purple | → | pink | Blue component confused with green |
set.violet | violet | → | red | Blue component confused with green |