Color

Color

One system: the Material Design 3 theme tokens.

Color in Expressive is the Material Design 3 Expressive roles — CSS custom properties on :root named --md-sys-color-*, exposed as utility classes like .primary and .on-surface-text. There are 26 standard roles in six groups (primary, secondary, tertiary, error, surface, outline), plus optional add-ons for fixed accents and the surface container scale.

The 2014 Material palette that shipped with earlier versions (.red, .blue.lighten-2, colorFunc()) has been removed. It generated 532 utility classes — 18% of the stylesheet — expressing a design opinion this framework does not hold, and it did not follow the page theme. Replace a palette class with the role it was standing in for: .red.error, .blue.primary, .green.tertiary, and pair each fill with its on-* text class.

Token values follow the page theme. The docs site sets <html theme="light">, theme="dark", or theme="auto". Auto (and omitting the attribute) follows the OS.

A background class sets background-color. Append -text for color. Pair roles as the spec intends so contrast stays at least 3:1 when the theme flips:

  • Primary — most prominent components: FAB, filled buttons, active states.
  • Secondary — less prominent components: filter chips, selected nav pills.
  • Tertiary — contrasting accents that balance primary and secondary.
  • Error — error states, not decoration.
  • Surface — page backgrounds. Surface container — cards, sheets, dialogs, menus.
  • On-* — text and icons on that fill. Never use a container role for text.
  • Variant — a lower-emphasis alternative to its pair (outline-variant, on-surface-variant).

Example: a filled button is primary + on-primary; a tonal button is secondary-container + on-secondary-container.

.primary
.on-primary
.primary-container
.on-primary-container
.secondary-container
.tertiary
.error
.surface-container
<div class="primary on-primary-text">.primary</div>
<span class="on-surface-text">.on-surface-text</span>

Every role name is a background class. The same name plus -text is the foreground class (.on-surface-text). Prefer the on-* text class on its paired fill.

In Sass, consume the token directly. Do not write rgba(var(--md-sys-color-primary), 0.06) — the tokens hold hex colors, so that form is invalid. Mix with transparency instead:

.my-tint {
  background-color: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
}

.my-overlay {
  background-color: color-mix(in oklab, var(--md-sys-color-primary) 6%, transparent);
}

Mix in oklab, not in srgb. sRGB interpolation dips in lightness through the midtones, so the same percentage reads muddier on some hues than others; OKLab is perceptually uniform, so a 16% state layer looks like 16% everywhere.

Every role

A role is a job, not a color: error means "this went wrong", and what that looks like is the theme's business. Each swatch uses its paired on-* color for the label.

Primary
.primary
.primary-container
.primary-fixed
.primary-fixed-dim
Secondary
.secondary
.secondary-container
.secondary-fixed
.secondary-fixed-dim
Tertiary
.tertiary
.tertiary-container
.tertiary-fixed
.tertiary-fixed-dim
Error
.error
.error-container
Surface
.surface-dim
.surface
.surface-bright
.surface-container-lowest
.surface-container-low
.surface-container
.surface-container-high
.surface-container-highest
Outline
.outline
.outline-variant
On and inverse
.on-surface
.on-surface-variant
.inverse-surface
.inverse-on-surface
.inverse-primary
Overlay
.scrim
.shadow
.surface-tint

scrim is the opaque neutral the wash behind a modal surface is mixed from, not the wash itself. That is --md-comp-scrim-color — the role at 32%, defined once at the root and consumed by dialogs, both sheets, the navigation drawer and the modal navigation rail. Override it at the root to move every scrim, or set it on one surface to dim just that one — ::backdrop inherits from the element it belongs to. The mix resolves at the root, so overriding --md-sys-color-scrim on a subtree does not reach a scrim below it; override --md-comp-scrim-color instead.

background, on-background, and surface-variant stay as aliases of surface / on-surface / the old neutral-variant well so existing pages do not break. New work should use the surface container scale.

Sass

Do not @extend these classes across files — that is how the old stylesheet lost control of cascade order. In a component partial, read the token directly.

.my-panel {
  background-color: var(--md-sys-color-surface-container);
  color: var(--md-sys-color-on-surface);
}

There is no Sass color function any more. colorFunc() and the $colors map were removed with the palette — a Sass function resolves at build time, which cannot follow a theme the user switches at runtime. The custom property can.

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.