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.
<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
Secondary
Tertiary
Error
Surface
Outline
On and inverse
Overlay
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.