Back to CSS Play/Pause Buttons Voice Memo Light JS
Share
HTML
<button class="pp-mem" type="button" aria-pressed="false" aria-label="Play voice memo" data-pp>
  <span class="pp-mem-dot" aria-hidden="true"></span>
  <span class="pp-mem-wave" aria-hidden="true">
    <span></span><span></span><span></span><span></span><span></span> <span></span><span></span
    ><span></span><span></span>
  </span>
  <span class="pp-mem-time">0:14</span>
</button>
CSS
.pp-mem {
  display: inline-grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 8px 14px 8px 10px;
  background: #15151d;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  color: #f0eeff;
  cursor: pointer;
  font-family: "JetBrains Mono", monospace;
  transition:
    border-color 0.2s,
    background 0.2s;
}
.pp-mem:hover {
  border-color: rgba(239, 68, 68, 0.4);
  background: #1a1a25;
}
.pp-mem:focus-visible {
  outline: 3px solid rgba(239, 68, 68, 0.4);
  outline-offset: 3px;
}
.pp-mem-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #ef4444;
  box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
  transition: background 0.2s;
}
.pp-mem[aria-pressed="true"] .pp-mem-dot {
  animation: ppMemPing 1.4s ease-out infinite;
}
@keyframes ppMemPing {
  0% {
    box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.6);
  }
  100% {
    box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);
  }
}
.pp-mem-wave {
  display: flex;
  align-items: center;
  gap: 2px;
  height: 18px;
}
.pp-mem-wave > span {
  width: 2px;
  height: 30%;
  background: #94a3b8;
  border-radius: 1px;
  transform-origin: center;
}
.pp-mem-wave > span:nth-child(1) {
  height: 50%;
}
.pp-mem-wave > span:nth-child(2) {
  height: 80%;
}
.pp-mem-wave > span:nth-child(3) {
  height: 65%;
}
.pp-mem-wave > span:nth-child(4) {
  height: 95%;
}
.pp-mem-wave > span:nth-child(5) {
  height: 70%;
}
.pp-mem-wave > span:nth-child(6) {
  height: 90%;
}
.pp-mem-wave > span:nth-child(7) {
  height: 55%;
}
.pp-mem-wave > span:nth-child(8) {
  height: 75%;
}
.pp-mem-wave > span:nth-child(9) {
  height: 40%;
}
.pp-mem[aria-pressed="true"] .pp-mem-wave > span {
  background: #ef4444;
  animation: ppMemWave 0.6s ease-in-out infinite alternate;
}
.pp-mem[aria-pressed="true"] .pp-mem-wave > span:nth-child(2n) {
  animation-delay: 0.1s;
}
.pp-mem[aria-pressed="true"] .pp-mem-wave > span:nth-child(3n) {
  animation-delay: 0.2s;
}
@keyframes ppMemWave {
  from {
    transform: scaleY(0.5);
  }
  to {
    transform: scaleY(1.1);
  }
}
.pp-mem-time {
  font-size: 11px;
  font-weight: 600;
  color: #94a3b8;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}

@media (prefers-reduced-motion: reduce) {
  .pp-mem,
  .pp-mem * {
    animation: none !important;
  }
}
JS
// ── 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');
  });
});