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.
| Category | Micro-interactions |
|---|---|
| Type | Web Component (<mv-pulse-ring>) |
| Status | stable |
| Keywords | ripple, ink, material, click, pulse, rings, radar, live |
When to use
- Buttons, list items or cards need tactile ink feedback spreading from the click point
- An avatar or live status indicator should be surrounded by radiating or breathing pulse rings
- Many clickable elements should get touch feedback through one class with no per-element setup
Avoid when
- A button needs a persistent attention effect like shine, glow or pulse at rest → use Button Effects instead
Install
node scripts/add.mjs ripple --out ./src/marvelousAI 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
| Name | Type | Default | Description |
|---|---|---|---|
data-mv-ripple | (empty) | center | off | Enables the ripple; center = always from the center (icon buttons); off = disables it on an element marked .mv-ripple. | |
count | number | 4 | <mv-pulse-ring>: number of rings (1–12). |
size | number | 96 | <mv-pulse-ring>: core diameter (px), where the rings start. |
gap | number | 44 | <mv-pulse-ring>: spacing between two rings (px). |
duration | number | 3200 | <mv-pulse-ring>: length of one wave cycle (ms). |
data-variant | breathe | <mv-pulse-ring>: fixed rings that contract in sequence (instead of radiating). | |
data-fill | boolean | <mv-pulse-ring>: fills the positioned parent as a backdrop, edges faded with a mask. |
Methods
| Name | Description |
|---|---|
ripple(el, { x, y, center }) | Export: creates a ripple in el from a viewport point; returns release(), which fades it out. |
Content structure
| Name | Description |
|---|---|
(mv-pulse-ring content) | Central element (avatar, icon, badge), placed above the rings. |
CSS classes
| Name | Description |
|---|---|
mv-ripple | On a button, link, card…: ink ripple on click (delegated listener, nothing to initialize). Same as data-mv-ripple. |
mv-ripple-ink | The generated ripple (styleable). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-ripple-color | currentColor | Ink color. |
--mv-ripple-opacity | 26% | Ink density. |
--mv-ripple-z | -1 | Ink layer (-1 = behind the text, above the host’s background). |
--mv-pulse-ring-color | var(--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.