Skip to content

ExpressiveCSS

Browse documentation

Color

Color roles from the Material Design 3 theme tokens.

View as Markdown

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.

.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, 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
.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

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.

No matching pages

  • 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.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow