Warning Pulse
A warning triangle flashes twice to catch the eye, then swells like a heartbeat and sends outlines of itself rippling outward, over and over until it’s dealt with. For low balances, expiring sessions and unsaved changes.
Session ending
Unsaved changes
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-warning-pulse></div>where the warning should appear. Itsfont-sizeis the size of the icon. - It pulses while it’s on screen. Call
animacious.warningPulse(el, false)to calm it once the problem is fixed, and(el, true)to start it again.
Options
Add any of these attributes to the same element.
data-color | Colour of the triangle and its ripples. Default #f59e0b. |
data-flashes | Quick flashes at the start of each beat. 0 just pulses. Default 2. |
data-speed | Time for one beat, flashes and pulse together. Default 1800 ms. |
data-once | Flash and pulse once, then stay still. |
It’s labelled “Warning” for screen readers; set your own aria-label. With reduced motion the triangle sits still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Warning Pulse: a warning triangle that flashes to catch the eye, then
// keeps pulsing, sending outlines of itself rippling outward, until it's
// dealt with. For low balance, expiring sessions and unsaved changes.
// Usage: <div data-warning-pulse></div> (size it with font-size: 1em = the icon)
// Optional: data-color (default #f59e0b), data-flashes (quick flashes at the
// start of each beat, default 2), data-speed (ms per beat, default 1800),
// data-once (flash and pulse once instead of repeating), aria-label
// (default "Warning").
// Script use: animacious.warningPulse(el, false) stops it, (el, true) starts it again.
var DEFAULTS = { color: '#f59e0b', flashes: 2, speed: 1800 };
var TRI = 'M26 5.5c1.6 0 3 .8 3.8 2.2l18.6 32.4c1.7 2.9-.4 6.4-3.8 6.4H7.4c-3.4 0-5.5-3.5-3.8-6.4L22.2 7.7c.8-1.4 2.2-2.2 3.8-2.2z';
var css =
'.wpu{position:relative;display:inline-block;width:1em;height:1em;vertical-align:middle;line-height:0}' +
'.wpu svg{position:absolute;inset:0;display:block;width:100%;height:100%;overflow:visible}' +
'.wpu-echo{pointer-events:none}' +
'.wpu-mark{transform-box:fill-box;transform-origin:50% 100%}' +
'.wpu-echo,.wpu-icon{transform-origin:50% 60%}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var c = d.color || DEFAULTS.color;
el.classList.add('wpu');
el.setAttribute('role', 'img');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Warning');
var echo = '<svg class="wpu-echo" viewBox="0 0 52 52" aria-hidden="true"><path d="' + TRI +
'" fill="none" stroke="' + c + '" stroke-width="2.4" stroke-linejoin="round"/></svg>';
el.innerHTML = echo + echo +
'<svg class="wpu-icon" viewBox="0 0 52 52" aria-hidden="true">' +
'<path d="' + TRI + '" fill="' + c + '"/>' +
'<g class="wpu-mark" fill="#fff"><rect x="23.6" y="17" width="4.8" height="15" rx="2.4"/><circle cx="26" cy="38.5" r="2.7"/></g>' +
'</svg>';
el._wpu = {
icon: el.querySelector('.wpu-icon'), mark: el.querySelector('.wpu-mark'),
echoes: el.querySelectorAll('.wpu-echo'), tl: null, on: false
};
anime.utils.set(el._wpu.echoes, { opacity: 0 });
}
function build(el) {
var s = el._wpu, d = el.dataset;
var speed = Math.max(900, +d.speed || DEFAULTS.speed);
var flashes = Math.max(0, Math.round(d.flashes !== undefined ? +d.flashes : DEFAULTS.flashes));
var once = d.once !== undefined && d.once !== 'false';
var tl = anime.createTimeline({ loop: !once, autoplay: false });
var t = 0;
// Flash: the triangle blinks off and on, the "!" with it.
for (var i = 0; i < flashes; i++) {
tl.add(s.icon, { opacity: [1, 0.2], duration: 90, ease: 'linear' }, t);
tl.add(s.icon, { opacity: [0.2, 1], duration: 110, ease: 'linear' }, t + 90);
t += 220;
}
// Pulse: a heartbeat swell while two outlines ripple out and fade.
tl.add(s.icon, {
scale: [{ to: 1.12, duration: 180, ease: 'out(2)' }, { to: 1, duration: 520, ease: 'outElastic(1, .55)' }]
}, t);
tl.add(s.mark, { scaleY: [{ to: 1.15, duration: 160 }, { to: 1, duration: 360, ease: 'out(3)' }] }, t);
tl.add(s.echoes[0], { scale: [1, 1.7], opacity: [0.7, 0], duration: 1000, ease: 'out(3)' }, t + 40);
tl.add(s.echoes[1], { scale: [1, 1.45], opacity: [0.5, 0], duration: 900, ease: 'out(3)' }, t + 220);
var end = Math.max(t + 1100, speed);
tl.add({ duration: 1 }, end - 1);
return tl;
}
function start(el) {
var s = el._wpu;
if (reduced()) return;
if (!s.tl) s.tl = build(el);
s.on = true;
s.tl.play();
}
function stop(el) {
var s = el._wpu;
s.on = false;
if (s.tl) s.tl.pause();
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._wpu;
if (e.isIntersecting) { if (!s.stopped) start(e.target); }
else if (s.on) stop(e.target);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-warning-pulse]').forEach(function (el) {
if (el._wpu) return;
setup(el);
if (!reduced()) io.observe(el);
});
}
window.animacious = window.animacious || {};
window.animacious.warningPulse = function (el, on) {
var s = el._wpu;
if (!s) return;
s.stopped = on === false;
if (s.stopped) {
stop(el);
if (s.tl) { s.tl.revert(); s.tl = null; }
anime.utils.set(s.echoes, { opacity: 0 });
} else start(el);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-warning-pulse style="font-size: 56px"></div>
<!-- just the pulse, a little faster: -->
<div data-warning-pulse data-flashes="0" data-speed="1400" aria-label="Unsaved changes"></div>