Skip to content

Material Design 3 Color System ​

Material Design 3 builds a whole color scheme from one source color: it derives a handful of tonal palettes in HCT, then assigns every UI role (primary, onPrimary, surface…) a fixed tone from one of those palettes. Coolors MCP implements the classic Material 3 core palette and tone mapping. It does not implement scheme variants or contrast levels.

What is implemented ​

ImplementedNot implemented
Core palette with Material's default ("tonal spot") chroma constantsScheme variants (vibrant, expressive, fidelity, content, monochrome, neutral…)
23 color roles for light and darkContrast levels (medium / high)
Tonal palettes at any tone 0–100Surface-container roles, outlineVariant, shadow, scrim, inverse roles
Hue harmonization, HCT blending, temperature shift (harmonize_colors)Elevation, shape or typography tokens

Tools: generate_material_theme, generate_theme_css, generate_tonal_palette, harmonize_colors, and generate_theme_from_image (see Image Analysis). Parameters are listed in Material Design tools.

Core palettes ​

Six tonal palettes are derived from the source color's HCT hue h and chroma c (corePaletteFromRgb in src/color/hct/tonal-palette.ts, same constants as Material Color Utilities' CorePalette.of):

PaletteHueChroma
Primaryhmax(48, c)
Secondaryh16
Tertiaryh + 60°24
Neutralh4
Neutral varianth8
Error25°84

Because primary chroma is at least 48, even a grey source produces a colorful primary. #808080 has HCT hue 209.5° and chroma 1.9, so its light-theme primary is #006874 (a teal).

Each palette is a fixed hue and chroma; a tone is resolved on request. If the chroma cannot be reached at that tone, it is reduced and the tone is kept (see HCT).

Standard tones ​

text
0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100

These are the defaults of generate_tonal_palette (tones) and generate_theme_css (includeTones). Both accept any tone from 0 to 100.

generate_tonal_palette is not the theme's primary palette

generate_tonal_palette keeps the seed's own hue and chroma, without the 48-chroma floor. For #6750a4 (chroma 47.9) its tone 70 is #b69df7, while the theme's primary palette (chroma 48) gives #b69df8.

Role → tone mapping ​

generate_material_theme output for sourceColor: "#6750a4":

RolePaletteLight toneLightDark toneDark
primaryprimary40#6750a480#cfbcff
onPrimaryprimary100#ffffff20#381e72
primaryContainerprimary90#e9ddff30#4f378a
onPrimaryContainerprimary10#22005d90#e9ddff
secondarysecondary40#625b7180#cbc2db
onSecondarysecondary100#ffffff20#332d41
secondaryContainersecondary90#e8def830#4a4458
onSecondaryContainersecondary10#1e192b90#e8def8
tertiarytertiary40#7e526080#efb8c8
onTertiarytertiary100#ffffff20#4a2532
tertiaryContainertertiary90#ffd9e330#633b48
onTertiaryContainertertiary10#31101d90#ffd9e3
errorerror40#ba1a1a80#ffb4ab
onErrorerror100#ffffff20#690005
errorContainererror90#ffdad630#93000a
onErrorContainererror10#41000290#ffdad6
backgroundneutral99#fffbff10#1c1b1e
onBackgroundneutral10#1c1b1e90#e6e1e6
surfaceneutral99#fffbff10#1c1b1e
onSurfaceneutral10#1c1b1e90#e6e1e6
surfaceVariantneutral variant90#e7e0eb30#49454e
onSurfaceVariantneutral variant30#49454e80#cac4cf
outlineneutral variant50#7a757f60#948f99

Compared with Material Color Utilities 0.3.0 (Scheme.light / Scheme.dark for the same source), 45 of the 46 values are identical. The exception is dark onErrorContainer: this server uses error tone 90 (#ffdad6), as Material's current dynamic colors do, while the legacy Scheme.dark uses tone 80 (#ffb4ab).

Contrast of the role pairs ​

Because roles are tone pairs, their contrast is predictable. Measured with check_contrast (algorithm: "both") on the #6750a4 theme:

Pair (foreground on background)TonesWCAGAPCA Lc
onPrimary on primary (light)100 / 406.44:1-86.9
onPrimaryContainer on container10 / 9013.25:185.9
onSurface on surface (light)N10 / N9916.73:1102.3
onSurfaceVariant on surfaceVariantNV30 / NV907.24:174.7
outline on surface (light)NV50 / N994.38:169.5
onError on error (light)100 / 406.46:1-85.3
onPrimary on primary (dark)20 / 807.70:166.1
onSurface on surface (dark)N90 / N1013.29:1-87.9
outline on surface (dark)NV60 / N105.43:1-41.5

outline is a non-text role; its target is 3:1.

Theme CSS ​

generate_theme_css writes every core palette at the requested tones (--{prefix}-{palette}-{tone}) and a light-only block of semantic aliases. Tones 10, 40, 90, 99 and 100 are always included because the aliases point at them. Excerpt for {"sourceColor": "#6750a4", "prefix": "md", "includeTones": [40]}:

text
:root {
  /* Primary Colors */
  --md-primary-10: #22005d;
  --md-primary-40: #6750a4;
  --md-primary-90: #e9ddff;
  --md-primary-99: #fffbff;
  --md-primary-100: #ffffff;
  …
}

/* Semantic Color Mappings (Light Theme) */
:root {
  --md-primary: var(--md-primary-40);
  --md-on-primary: var(--md-primary-100);
  --md-primary-container: var(--md-primary-90);
  --md-on-primary-container: var(--md-primary-10);
  …
  --md-surface: var(--md-neutral-99);
  --md-on-surface: var(--md-neutral-10);
}

The aliases cover primary, secondary, background and surface only. For dark-theme values, use generate_material_theme.

Disliked source colors ​

Material's dislike rule flags dark yellow-greens: HCT hue 90–111°, chroma > 16, tone < 65. The fix keeps hue and chroma and sets the tone to 70 (#6b6b1e → #b0af5b).

  • generate_material_theme and generate_theme_css use the source color as given.
  • generate_theme_from_image fixes a disliked source before building the theme.
  • analyze_color_likability and fix_disliked_colors_batch apply the rule on demand.

Harmonization ​

harmonize_colors has three methods:

MethodWhat it does
harmonize (default)Keeps the first color. Rotates each other color's HCT hue toward it by at most 15°; below that cap the rotation is difference × factor (Material's Blend.harmonize uses 0.5). Chroma and tone are unchanged.
blendInterpolates hue (shortest way), chroma and tone by factor, folding all colors into one result color (#6750a4, #ff0000, #00ff00 → #ef8b00).
temperatureShifts every hue toward 30° (warmer, factor > 0.5) or 210° (cooler, factor < 0.5); factor 0.5 leaves colors unchanged (#6750a4, #ff0000 → #6750a4, #ff0000).
json
{ "colors": ["#6750a4", "#ff0000", "#00ff00"] }
text
Harmonized Colors (harmonize):
Original: #6750a4, #ff0000, #00ff00
Result: #6750a4, #fb0057, #00fc94

Hue differences are measured the short way around the circle, so colors on either side of 0°/360° work as expected (the implementation is identical to Material's Blend.harmonize). With ["hct(350, 60, 50)", "hct(10, 60, 50)"] the two hues are 20° apart, so the second color rotates 10° toward the first: #c44d7b, hue 359.8°.

See Also ​

Released under the MIT License.