Skip to content

Ripple

The ripple is Material’s press feedback: a circle that grows from the pointer. 24 components use it. It is on by default and can be switched off at any level.

<html data-ripple="off">

Every ripple in the app stops spawning. Hover, focus and pressed state layers still work — only the animated circle is suppressed.

Ripple on vs off — press each button
on Press me
off Press me
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- 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 me</md-button>
  <md-icon-button icon="favorite" aria-label="Favourite"></md-icon-button>
</div>
<div data-ripple="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 me</md-button>
  <md-icon-button icon="favorite" aria-label="Favourite"></md-icon-button>
</div>

The switch resolves to one custom property, --md-sys-ripple-enabled (1 on, 0 off). It travels both ways, so a subtree or a single component can opt back in inside a disabled region:

<html data-ripple="off">
<!-- no ripples anywhere -->
<section data-ripple="on">
<!-- ...except in here -->
<md-button variant="filled">Ripples</md-button>
</section>
<md-button variant="filled" ripple="on">Also ripples</md-button>
</html>
ScopeAttributeWhere
Globaldata-ripple="off" / "on"<html> or any ancestor
Regiondata-ripple="off" / "on"Any wrapper element
Componentripple="off" / "on"The component host

Both spellings resolve to the same property, so a component never needs to know which one was used.

A local rule applies the property directly to the host; the global one arrives by inheritance from an ancestor. A directly-applied custom property always beats an inherited one, at any specificity — no !important and no specificity tricks.

The obvious implementation — reading an ancestor’s state from inside the shadow root — is not available: Firefox does not implement :host-context(). An earlier version of this library used it and was silently inert in Firefox. The inherited-custom-property approach works in every browser and needs no JavaScript.

// off
document.documentElement.dataset.ripple = 'off';
// back on
document.documentElement.dataset.ripple = 'on';
// or remove the attribute entirely for the default
delete document.documentElement.dataset.ripple;

Because it is pure CSS, the change applies instantly with no re-render.

The ripple is animation. Respect the OS preference:

@media (prefers-reduced-motion: reduce) {
:root { --md-sys-ripple-enabled: 0; }
}
SituationRecommendation
Utilitarian internal tool, flat visual languageGlobal off
Dense data grid where every cell click ripplesoff on the table region
User has prefers-reduced-motionoff via the media query above
Consumer productLeave it on — it is a core Material affordance

If you build your own interactive surface and want matching feedback, use the md-ripple primitive rather than a hand-rolled animation — it participates in this same global switch:

<div class="my-tile" style="position: relative; overflow: hidden;">
<md-ripple></md-ripple>
…content…
</div>

Note that pointer input ripples automatically but keyboard activation does not — call trigger() from your Enter/Space handler, which is what the library components do.