← Feedback

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

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-warning-pulse></div> where the warning should appear. Its font-size is the size of the icon.
  3. 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-colorColour of the triangle and its ripples. Default #f59e0b.
data-flashesQuick flashes at the start of each beat. 0 just pulses. Default 2.
data-speedTime for one beat, flashes and pulse together. Default 1800 ms.
data-onceFlash 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.

1 · Snippet
<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>
2 · Your HTML
<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>