Color in Expressive uses the Material Design 3 Expressive roles.
They are 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 from earlier versions
(.red, .blue.lighten-2, colorFunc())
is gone. It generated 532 utility classes, 18% of the stylesheet.
Those classes expressed a design opinion this framework does not hold,
and they did not follow the page theme. Replace a palette class with the
role it stood in for: .red becomes .error,
.blue becomes .primary, and .green
becomes .tertiary. 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 is for the most prominent components: FAB, filled buttons, and active states.
- Secondary is for less prominent components, such as filter chips and selected nav pills.
- Tertiary is for contrasting accents that balance primary and secondary.
- Error is for error states, not decoration.
- Surface is for page backgrounds. Surface container is for cards, sheets, dialogs, and menus.
- On-* is for text and icons on that fill. Never use a container role for text.
- Variant is a lower-emphasis alternative to its pair (
outline-variant,on-surface-variant).
For example, a filled button uses primary with
on-primary, and a tonal button uses
secondary-container with
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, use 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 describes a purpose. error means "this went
wrong", and the theme decides what that looks like.
Each swatch uses its paired on-* color for the label.
Primary
Secondary
Tertiary
Error
Surface
Outline
On and inverse
Overlay
To change a scrim, override --md-comp-scrim-color. It is
the dimming layer behind a modal surface. Its value is the
scrim role at 32%, defined once at the root, and dialogs,
both sheets, and the modal navigation rail use it. Override it at the
root to change every scrim. Set it on one surface to change only that
surface's scrim, because ::backdrop inherits from the
element it belongs to.
The scrim role is the opaque neutral that the dimming layer
is mixed from. 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.
Sass
In a component partial, read the token directly. Do not
@extend these classes across files. The old stylesheet
lost control of cascade order that way.
.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, because a Sass function
resolves at build time and cannot follow a theme the user switches at
runtime. A custom property can.