← Feedback

Error Cross

A circle draws itself round, two quick strokes strike through it to make a cross, and the icon gives a short “no” shake. For failed payments, rejected forms and anything else that didn’t work. Outline or filled, sized with font-size.

Couldn’t save

Payment failed

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-error-cross></div> where the cross should appear. Its font-size is the size of the icon.
  3. It draws when it scrolls into view, or call animacious.errorCross(el) to draw it again after another failed try.

Options

Add any of these attributes to the same element.

data-colorColour of the circle and cross. Default #dc2626.
data-fillFill the circle in behind a white cross.
data-shake="false"Pop instead of shaking once the cross lands.
data-durationHow long the circle takes to draw. Default 650 ms.
data-loopFade away after a pause and draw again.
data-holdHow long the finished cross shows before looping. Default 1800 ms.

It’s labelled “Error” for screen readers; set your own aria-label to say what went wrong. With reduced motion the finished cross simply appears.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Error Cross: a circle draws itself round, then two strokes strike through
    // it to make a cross, and the whole thing gives a short "no" shake.
    // For failed payments, rejected forms and anything that didn't work.
    // Usage: <div data-error-cross></div>   (size it with font-size: 1em = the icon)
    // Optional: data-color (default #dc2626), data-fill (the circle fills in
    // behind a white cross), data-shake="false" (no shake), data-duration (ms
    // the circle takes, default 650), data-loop (fade away and draw again),
    // data-hold (ms the finished cross shows before looping, default 1800),
    // aria-label (default "Error").
    // Script use: animacious.errorCross(el) draws it again.
    var DEFAULTS = { color: '#dc2626', duration: 650, hold: 1800 };

    var css =
      '.ecx{position:relative;display:inline-block;width:1em;height:1em;vertical-align:middle;line-height:0}' +
      '.ecx-bg{transform-box:fill-box;transform-origin:50% 50%}' +
      '.ecx svg{position:relative;display:block;width:100%;height:100%;overflow:visible}';
    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;
      var fill = d.fill !== undefined && d.fill !== 'false';
      var ink = fill ? '#fff' : c;
      el.classList.add('ecx');
      el.setAttribute('role', 'img');
      if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Error');
      el.innerHTML = '<svg viewBox="0 0 52 52" aria-hidden="true">' +
        '<circle class="ecx-bg" cx="26" cy="26" r="23" fill="' + c + '"/>' +
        '<path class="ecx-circle" d="M26 3a23 23 0 1 1 0 46a23 23 0 1 1 0-46" fill="none" stroke="' + c + '" stroke-width="3.2" stroke-linecap="round"/>' +
        '<path class="ecx-line" d="M18 18L34 34" fill="none" stroke="' + ink + '" stroke-width="4" stroke-linecap="round"/>' +
        '<path class="ecx-line" d="M34 18L18 34" fill="none" stroke="' + ink + '" stroke-width="4" stroke-linecap="round"/>' +
        '</svg>';
      var lines = el.querySelectorAll('.ecx-line');
      var s = el._ecx = {
        svg: el.querySelector('svg'), bg: el.querySelector('.ecx-bg'),
        circle: anime.svg.createDrawable(el.querySelector('.ecx-circle')),
        lines: [anime.svg.createDrawable(lines[0])[0], anime.svg.createDrawable(lines[1])[0]],
        timer: null, run: 0
      };
      anime.utils.set(s.bg, { opacity: fill ? 1 : 0.1 });
      if (reduced()) return;
      anime.utils.set(s.circle, { draw: '0 0' });
      anime.utils.set(s.lines, { draw: '0 0' });
      anime.utils.set(s.bg, { scale: 0 });
    }

    function play(el) {
      var s = el._ecx, d = el.dataset;
      var dur = +d.duration || DEFAULTS.duration;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var run = ++s.run;
      clearTimeout(s.timer);
      anime.utils.set(s.svg, { opacity: 1, scale: 1, x: 0 });
      anime.utils.set(s.circle, { draw: '0 0' });
      anime.utils.set(s.lines, { draw: '0 0' });
      anime.utils.set(s.bg, { scale: 0 });

      anime.animate(s.circle, { draw: ['0 0', '0 1'], duration: dur, ease: 'inOut(2)' });
      anime.animate(s.bg, { scale: [0, 1], duration: dur * 0.6, delay: dur * 0.75, ease: 'outBack(1.4)' });
      // Two quick strikes, like a pen crossing something out.
      anime.animate(s.lines[0], { draw: ['0 0', '0 1'], duration: 220, delay: dur * 0.9, ease: 'in(1.6)' });
      anime.animate(s.lines[1], { draw: ['0 0', '0 1'], duration: 220, delay: dur * 0.9 + 200, ease: 'in(1.6)' });
      var landed = dur * 0.9 + 420;
      if (d.shake !== 'false') {
        anime.animate(s.svg, {
          x: ['0em', '-0.09em', '0.08em', '-0.06em', '0.04em', '-0.02em', '0em'],
          duration: 520, delay: landed, ease: 'inOut(1.5)'
        });
      } else {
        anime.animate(s.svg, { scale: [{ to: 1.08, duration: 140 }, { to: 1, duration: 500, ease: 'outElastic(1, .5)' }], delay: landed });
      }

      if (!loop) return;
      s.timer = setTimeout(function () {
        if (run !== s.run) return;
        anime.animate(s.svg, {
          opacity: 0, scale: 0.85, duration: 350, ease: 'in(2)',
          onComplete: function () { if (run === s.run) s.timer = setTimeout(function () { play(el); }, 350); }
        });
      }, landed + 520 + (+d.hold || DEFAULTS.hold));
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target);
        });
      }, { threshold: 0.4 });
      document.querySelectorAll('[data-error-cross]').forEach(function (el) {
        if (el._ecx) return;
        setup(el);
        if (!reduced()) io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.errorCross = function (el) { if (el._ecx && !reduced()) play(el); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-error-cross style="font-size: 64px"></div>

<!-- filled, without the shake: -->
<div data-error-cross data-fill data-shake="false" aria-label="Payment failed"></div>