CSS Copy
.pp-ripple {
position: relative;
width: 56px;
height: 56px;
border: 0;
padding: 0;
background: linear-gradient(135deg, #a78bfa, #8b5cf6);
border-radius: 50%;
color: white;
cursor: pointer;
display: grid;
place-items: center;
overflow: hidden;
box-shadow: 0 6px 16px -4px rgba(139, 92, 246, 0.55);
transition:
transform 0.15s,
box-shadow 0.25s;
}
.pp-ripple:hover {
transform: translateY(-1px);
box-shadow: 0 10px 22px -4px rgba(139, 92, 246, 0.7);
}
.pp-ripple:focus-visible {
outline: 3px solid rgba(139, 92, 246, 0.5);
outline-offset: 3px;
}
.pp-ripple-wave {
position: absolute;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
transform: translate(-50%, -50%);
pointer-events: none;
animation: ppRippleOut 0.6s ease-out forwards;
}
@keyframes ppRippleOut {
to {
width: 140px;
height: 140px;
opacity: 0;
}
}
.pp-ripple-icon {
position: relative;
z-index: 1;
transition: d 0.3s cubic-bezier(0.5, 0, 0.3, 1.2);
}
.pp-ripple[aria-pressed="true"] .pp-ripple-icon {
d: path("M7 5 H10 V19 H7 Z M14 5 H17 V19 H14 Z");
}
/* ── Drop-in JS for the ripple ──
document.querySelectorAll('[data-pp-ripple]').forEach(btn => {
btn.addEventListener('pointerdown', e => {
const r = btn.getBoundingClientRect();
const wave = document.createElement('span');
wave.className = 'pp-ripple-wave';
wave.style.left = (e.clientX - r.left) + 'px';
wave.style.top = (e.clientY - r.top) + 'px';
btn.appendChild(wave);
wave.addEventListener('animationend', () => wave.remove());
});
});
*/
@media (prefers-reduced-motion: reduce) {
.pp-ripple,
.pp-ripple * {
animation: none !important;
}
}
JS Copy
// ── Drop this on every page where you render a play/pause button ──
// Toggles aria-pressed + aria-label on click. The CSS handles all visuals.
document.querySelectorAll('[data-pp]').forEach(function (btn) {
btn.addEventListener('click', function () {
var playing = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!playing));
btn.setAttribute('aria-label', !playing ? 'Pause' : 'Play');
});
});