Skip to content

Shape Morph

Shape morph is the M3 Expressive behaviour where a control’s corner radius animates on press or selection — a round button squaring off as you push it. It is on by default.

<html data-shape-morph="off">

Shapes freeze at their resting radius. Nothing else changes: the control still presses, still ripples, still shows its state layer.

Shape morph on vs off — press and hold, then try the toggles
on Press and hold Toggle
off Press and hold Toggle
Show code for each technology
<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display:flex;gap:12px;align-items:center;margin-block-end:16px;">
  <span style="width:5rem;opacity:.7;font-size:.8rem;">on</span>
  <md-button variant="filled">Press and hold</md-button>
  <md-button variant="tonal" toggle>Toggle</md-button>
</div>
<div data-shape-morph="off" style="display:flex;gap:12px;align-items:center;">
  <span style="width:5rem;opacity:.7;font-size:.8rem;">off</span>
  <md-button variant="filled">Press and hold</md-button>
  <md-button variant="tonal" toggle>Toggle</md-button>
</div>

Same model as ripple: one custom property, --md-sys-shape-morph-enabled (1 on, 0 frozen), settable at any level and reversible at any level.

<html data-shape-morph="off">
<section data-shape-morph="on">
<md-button variant="filled">Morphs</md-button>
</section>
<md-button variant="filled" shape-morph="on">Also morphs</md-button>
</html>
ScopeAttributeWhere
Globaldata-shape-morph="off" / "on"<html> or any ancestor
Regiondata-shape-morph="off" / "on"Any wrapper element
Componentshape-morph="off" / "on"The component host

The value is 0/1 rather than a keyword so CSS can use it as arithmetic. Each morphed radius is blended back toward its resting value:

border-radius: calc(
REST + var(--md-sys-shape-morph-enabled, 1) * (MORPHED - REST)
);

At 1 that is exactly MORPHED; at 0 it collapses to REST. The toggle stays in pure CSS, so flipping it applies instantly with no re-render — and it avoids :host-context(), which Firefox does not implement.

Only 7 components have shape-morph behaviour; the attribute is a no-op everywhere else:

md-button · md-icon-button · md-fab · md-fab-menu · md-segmented-button · md-split-button · md-card

Two distinct triggers:

  • Pressed — the radius squares off by one step while held.
  • Selected (toggle) — a round control morphs to square when selected; a square one morphs to round.
document.documentElement.dataset.shapeMorph = 'off'; // freeze
document.documentElement.dataset.shapeMorph = 'on'; // restore
delete document.documentElement.dataset.shapeMorph; // default (on)

Note the DOM property is camelCased (dataset.shapeMorph) for the data-shape-morph attribute.

@media (prefers-reduced-motion: reduce) {
:root { --md-sys-shape-morph-enabled: 0; }
}

Pair it with the same guard for ripple to cover both expressive behaviours:

@media (prefers-reduced-motion: reduce) {
:root {
--md-sys-ripple-enabled: 0;
--md-sys-shape-morph-enabled: 0;
}
}
SituationRecommendation
Utilitarian / enterprise visual languageGlobal off
Brand guidelines specify static geometryGlobal off
User prefers reduced motionoff via the media query
Consumer product wanting the M3 Expressive feelLeave it on
  • Ripple — the other expressive toggle, same mechanism
  • Design Tokens — the shape corner scale