Skip to content

Ripple for plain HTML

m3e-css draws the hover, focus and pressed states without JavaScript. The press ripple that spreads from the pointer needs one small import.

One import

import '@kyralabs/m3e-elements/ripple.js';

It adds the ripple to every plain-HTML button, icon button, chip (assist, filter, input, suggestion), FAB, extended FAB, FAB menu item, .m3e-state-layer, interactive card, interactive list item, navigation item, navigation drawer item, tab, checkbox, radio button and switch in the document, including ones added later. One set of delegated pointer and keyboard listeners on document does it all.

This page loads it; press these:

How it behaves

  • The first press on a host sets data-ripple, which stays; each press toggles data-pressed and writes --_m3e-ripple-x, -y and -radius, which m3e-css animates.
  • Enter and Space press from the center. Links press on Enter only; checkboxes, filter chips, radio buttons and switches on Space only, since that is the key that toggles them.
  • Hosts with a data-ripple it did not set are skipped: m3e-react components and custom elements draw their own. Disabled hosts are skipped too.
  • Reduced motion and forced colors are handled by m3e-css: the ripple follows the motion tokens, and forced colors hide the overlay.

Scope it or stop it

attachAutoRipple(root, signal) from the package root does the same for one subtree, a shadow root or the document, without attaching to the document by itself. Aborting the signal removes the attributes and variables it wrote. The package root also registers every element, so use it where you load the elements anyway.

import { attachAutoRipple } from '@kyralabs/m3e-elements';

const controller = new AbortController();
attachAutoRipple(document.querySelector('#toolbar')!, controller.signal);
// later
controller.abort();

React components and the custom elements already include their ripple; the import is only for markup you write with m3e-css classes.