Speaker Volume Waves
Sound waves ripple out from a speaker icon, one after another. Tap it to mute: the waves shrink away into the speaker and a cross draws itself in, stroke by stroke. Tap again and the cross wipes off as the waves come back.
Tap the speaker to mute or unmute
How to use it
- Paste the snippet just before
</head>. - Add
data-speaker-volume-wavesto an empty<button>with anaria-labelsuch asMute. Size it withfont-sizeand colour it withcolor.
Options
Add any of these attributes to the same element.
data-muted | Start muted. |
data-autoplay | Mute and unmute every couple of seconds while in view, until someone taps it. Handy for demos. |
It’s a real toggle: aria-pressed is true while muted. Listen for the toggle event (e.detail.muted) to mute your media, or call animacious.speaker(button) to toggle it from code. The ripple only runs while on screen, and not at all with reduced motion.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Speaker Volume Waves: sound waves ripple out from a speaker icon. Tap it
// to mute: the waves shrink away and a cross draws itself in; tap again
// and the cross wipes off as the waves come back.
// Usage: <button data-speaker-volume-waves></button>
// Size it with font-size, colour it with color. aria-pressed is true
// while muted.
// Optional: data-muted (start muted), data-autoplay (it mutes and unmutes
// itself every couple of seconds while in view, until someone taps it).
// Listen for the "toggle" event: e.detail.muted is true or false.
// Script use: animacious.speaker(button) toggles it from code.
var DEFAULTS = { every: 2200 };
var NS = 'http://www.w3.org/2000/svg';
var css =
'.svw{display:inline-grid;place-items:center;width:1.25em;height:1.25em;padding:0;border:0;background:none;color:inherit;font:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.svw svg{display:block;width:1em;height:1em;overflow:visible;pointer-events:none}' +
'.svw-cone{transform-origin:20px 32px}' +
'.svw-waves{transform-origin:34px 32px;transition:opacity .3s ease,transform .45s cubic-bezier(.3,1.4,.5,1)}' +
'.svw-wave{animation:svw-ripple 1.5s ease-in-out infinite}' +
'.svw-wave:nth-child(2){animation-delay:.18s}.svw-wave:nth-child(3){animation-delay:.36s}' +
'.svw-off .svw-wave,.svw-muted .svw-wave{animation-play-state:paused}' +
'.svw-muted .svw-waves{opacity:0;transform:scale(.4)}' +
'.svw-x{stroke-dasharray:20;stroke-dashoffset:20;transition:stroke-dashoffset .22s ease-in}' +
'.svw-muted .svw-x{stroke-dashoffset:0;transition:stroke-dashoffset .26s ease-out .16s}' +
'.svw-muted .svw-x+.svw-x{transition-delay:.36s}' +
'@keyframes svw-ripple{0%,100%{opacity:.3;transform:translateX(0)}35%{opacity:1;transform:translateX(1.5px)}}' +
'@media (prefers-reduced-motion:reduce){.svw-wave{animation:none}.svw-waves,.svw-x{transition:none!important}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function arc(r) {
var a = 50 * Math.PI / 180, c = Math.cos(a) * r, s = Math.sin(a) * r;
return 'M' + (34 + c).toFixed(2) + ' ' + (32 - s).toFixed(2) + ' A' + r + ' ' + r + ' 0 0 1 ' + (34 + c).toFixed(2) + ' ' + (32 + s).toFixed(2);
}
function setup(el) {
el.classList.add('svw', 'svw-off');
if (el.tagName === 'BUTTON' && !el.getAttribute('type')) el.type = 'button';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 64 64');
svg.setAttribute('aria-hidden', 'true');
svg.innerHTML =
'<path class="svw-cone" d="M9 24 H18 L31 13 V51 L18 40 H9 A3 3 0 0 1 6 37 V27 A3 3 0 0 1 9 24 Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>' +
'<g class="svw-waves" fill="none" stroke="currentColor" stroke-width="4.2" stroke-linecap="round">' +
'<path class="svw-wave" d="' + arc(8) + '"/><path class="svw-wave" d="' + arc(15.5) + '"/><path class="svw-wave" d="' + arc(23) + '"/></g>' +
'<g fill="none" stroke="currentColor" stroke-width="4.2" stroke-linecap="round">' +
'<path class="svw-x" pathLength="20" d="M41 25 L55 39"/><path class="svw-x" pathLength="20" d="M55 25 L41 39"/></g>';
el.appendChild(svg);
var s = { el: el, cone: svg.querySelector('.svw-cone'), muted: false };
el._svw = s;
set(s, el.dataset.muted !== undefined && el.dataset.muted !== 'false');
return s;
}
function set(s, muted) {
s.muted = muted;
s.el.classList.toggle('svw-muted', muted);
if (s.el.closest('[aria-hidden="true"]')) return;
if (!s.el.getAttribute('aria-label')) s.el.setAttribute('aria-label', 'Mute');
if (s.el.tagName === 'BUTTON') s.el.setAttribute('aria-pressed', muted);
}
function toggle(s) {
set(s, !s.muted);
s.el.dispatchEvent(new CustomEvent('toggle', { bubbles: true, detail: { muted: s.muted } }));
if (!reduced()) anime.animate(s.cone, { scale: [{ to: 0.86, duration: 110, ease: 'out(2)' }, { to: 1, duration: 520, ease: 'outElastic(1, .5)' }] });
}
function init() {
// The ripple only runs while on screen.
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._svw;
e.target.classList.toggle('svw-off', !e.isIntersecting);
clearTimeout(s.timer);
if (e.isIntersecting && s.auto) s.timer = setTimeout(s.tick, 900 + Math.random() * 500);
});
});
document.querySelectorAll('[data-speaker-volume-waves]').forEach(function (el) {
if (el._svw) return;
var s = setup(el);
el.addEventListener('click', function () {
s.auto = false;
clearTimeout(s.timer);
toggle(s);
});
if (el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false' && !reduced()) {
s.auto = true;
s.tick = function () {
if (!s.auto) return;
if (!el.matches(':hover') && document.activeElement !== el) toggle(s);
s.timer = setTimeout(s.tick, s.muted ? DEFAULTS.every * 0.7 : DEFAULTS.every);
};
}
io.observe(el);
});
}
window.animacious = window.animacious || {};
window.animacious.speaker = function (el) { if (el._svw) toggle(el._svw); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button data-speaker-volume-waves aria-label="Mute" style="font-size: 48px"></button>
<script>
document.querySelector("[data-speaker-volume-waves]").addEventListener("toggle", (e) => {
video.muted = e.detail.muted;
});
</script>