Ripple — <mv-pulse-ring>

Material-style ink ripple from the click point on any element (.mv-ripple / data-mv-ripple, grows while held), plus <mv-pulse-ring>: concentric rings that radiate or breathe.

CategoryMicro-interactions
TypeWeb Component (<mv-pulse-ring>)
Statusstable
Keywordsripple, ink, material, click, pulse, rings, radar, live

When to use

Avoid when

Install

node scripts/add.mjs ripple --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["ripple"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/element.js, core/motion.js, components/ripple/ripple.js, components/ripple/ripple.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1.5rem;align-items:center;width:100%;max-width:820px">
  <div style="display:grid;gap:1rem;justify-items:center">
    <div style="display:flex;flex-wrap:wrap;gap:.6rem;justify-content:center">
      <button class="mv-button mv-ripple" data-size="lg">Book now</button>
      <button class="mv-button mv-ripple" data-variant="outline" data-size="lg">Maybe later</button>
      <button class="mv-button mv-ripple" data-variant="ghost" data-size="icon" data-shape="round" data-mv-ripple="center" aria-label="Share">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 13.5 6.8 4M15.4 6.5l-6.8 4"/></svg>
      </button>
    </div>
    <div role="list" style="width:100%;max-width:300px;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);overflow:hidden;background:var(--mv-surface)">
      <a role="listitem" href="#" data-mv-ripple style="display:flex;gap:.75rem;align-items:center;padding:.75rem 1rem;color:inherit;text-decoration:none;--mv-ripple-color:var(--mv-accent)">
        <span style="width:2rem;height:2rem;border-radius:50%;display:grid;place-items:center;background:var(--mv-accent-subtle);color:var(--mv-accent-fg);font-weight:600;font-size:.8rem">OP</span>
        <span style="display:grid"><strong style="font-size:.875rem">Olivia Park</strong><span style="font-size:.78rem;color:var(--mv-fg-muted)">Flight New York → Lisbon, May 14</span></span>
      </a>
      <a role="listitem" href="#" data-mv-ripple style="display:flex;gap:.75rem;align-items:center;padding:.75rem 1rem;color:inherit;text-decoration:none;border-top:1px solid var(--mv-border);--mv-ripple-color:var(--mv-accent)">
        <span style="width:2rem;height:2rem;border-radius:50%;display:grid;place-items:center;background:var(--mv-bg-muted);font-weight:600;font-size:.8rem">ML</span>
        <span style="display:grid"><strong style="font-size:.875rem">Marcus Lee</strong><span style="font-size:.78rem;color:var(--mv-fg-muted)">Hotel in Porto, 3 nights</span></span>
      </a>
    </div>
  </div>

  <div style="display:flex;gap:1rem;justify-content:center;align-items:center;flex-wrap:wrap">
    <mv-pulse-ring count="3" size="64" gap="26">
      <span style="display:grid;place-items:center;width:64px;height:64px;border-radius:50%;background:var(--mv-accent);color:var(--mv-fg-on-accent);font:600 .68rem/1 var(--mv-font-sans);letter-spacing:.08em;text-transform:uppercase;box-shadow:var(--mv-shadow-lg)">Live</span>
    </mv-pulse-ring>
    <mv-pulse-ring data-variant="breathe" count="4" size="56" gap="18" style="--mv-pulse-ring-color:var(--mv-success)">
      <span style="display:grid;place-items:center;width:56px;height:56px;border-radius:50%;background:var(--mv-surface-raised);border:1px solid var(--mv-border);color:var(--mv-success);box-shadow:var(--mv-shadow-md)" role="img" aria-label="Connected">
        <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5a10 10 0 0 1 14 0M8.5 16a5 5 0 0 1 7 0M2 9a15 15 0 0 1 20 0"/><circle cx="12" cy="19.5" r="1"/></svg>
      </span>
    </mv-pulse-ring>
  </div>
</div>

API

Attributes

NameTypeDefaultDescription
data-mv-ripple(empty) | center | offEnables the ripple; center = always from the center (icon buttons); off = disables it on an element marked .mv-ripple.
countnumber4<mv-pulse-ring>: number of rings (1–12).
sizenumber96<mv-pulse-ring>: core diameter (px), where the rings start.
gapnumber44<mv-pulse-ring>: spacing between two rings (px).
durationnumber3200<mv-pulse-ring>: length of one wave cycle (ms).
data-variantbreathe<mv-pulse-ring>: fixed rings that contract in sequence (instead of radiating).
data-fillboolean<mv-pulse-ring>: fills the positioned parent as a backdrop, edges faded with a mask.

Methods

NameDescription
ripple(el, { x, y, center })Export: creates a ripple in el from a viewport point; returns release(), which fades it out.

Content structure

NameDescription
(mv-pulse-ring content)Central element (avatar, icon, badge), placed above the rings.

CSS classes

NameDescription
mv-rippleOn a button, link, card…: ink ripple on click (delegated listener, nothing to initialize). Same as data-mv-ripple.
mv-ripple-inkThe generated ripple (styleable).

CSS variables

NameDefaultDescription
--mv-ripple-colorcurrentColorInk color.
--mv-ripple-opacity26%Ink density.
--mv-ripple-z-1Ink layer (-1 = behind the text, above the host’s background).
--mv-pulse-ring-colorvar(--mv-accent)Ring color.

Accessibility

The ripple is aria-hidden with no pointer-events; it also follows keyboard activation (Enter/Space → ripple from the center) and ignores :disabled / aria-disabled. The host gets overflow: hidden and isolation. Reduced motion: no growing circle, just a brief tint of the surface; <mv-pulse-ring> rings stay still (concentric layout). The rings are decorative (aria-hidden): give the central content an accessible name if it carries meaning.