← Feedback

Success Tick

A circle draws itself round, a tick follows it in, and the icon gives a little pop as a soft ring ripples away. Just the thing for “Saved”, “Sent” and “Paid” moments. Outline or filled, any colour, sized with font-size.

Saved

Payment sent

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-success-tick></div> where the tick should appear. Its font-size is the size of the icon.
  3. It draws when it scrolls into view. To show it after an action, add it to the page then, or call animacious.successTick(el) to draw it again.

Options

Add any of these attributes to the same element.

data-colorColour of the circle and tick. Default #16a34a.
data-fillFill the circle in behind a white tick.
data-durationHow long the circle takes to draw. The tick follows in proportion. Default 650 ms.
data-loopFade away after a pause and draw again.
data-holdHow long the finished tick shows before looping. Default 1800 ms.

It’s labelled “Success” for screen readers; set your own aria-label to say what succeeded. With reduced motion the finished tick simply appears.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Success Tick: a circle draws itself round, then a tick follows it in with
    // a little pop and a soft ripple. For "saved", "sent" and "paid" moments.
    // Usage: <div data-success-tick></div>   (size it with font-size: 1em = the icon)
    // Optional: data-color (default #16a34a), data-fill (the circle fills in
    // behind a white tick), data-duration (ms the circle takes, default 650),
    // data-loop (fade away and draw again), data-hold (ms the finished tick
    // shows before looping, default 1800), aria-label (default "Success").
    // Script use: animacious.successTick(el) draws it again.
    var DEFAULTS = { color: '#16a34a', duration: 650, hold: 1800 };

    var css =
      '.stk{position:relative;display:inline-block;width:1em;height:1em;vertical-align:middle;line-height:0}' +
      '.stk-bg{transform-box:fill-box;transform-origin:50% 50%}' +
      '.stk svg{position:relative;display:block;width:100%;height:100%;overflow:visible}' +
      '.stk-ring{position:absolute;inset:0;border-radius:50%;border:.03em solid var(--stk-c);opacity:0;pointer-events:none}';
    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';
      el.classList.add('stk');
      el.style.setProperty('--stk-c', c);
      el.setAttribute('role', 'img');
      if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Success');
      // The circle starts at the top and runs clockwise; the tick runs short arm first.
      el.innerHTML = '<span class="stk-ring" aria-hidden="true"></span>' +
        '<svg viewBox="0 0 52 52" aria-hidden="true">' +
        '<circle class="stk-bg" cx="26" cy="26" r="23" fill="' + c + '"/>' +
        '<path class="stk-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="stk-tick" d="M15.5 27l7 7L37 19.5" fill="none" stroke="' + (fill ? '#fff' : c) + '" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>' +
        '</svg>';
      var s = el._stk = {
        svg: el.querySelector('svg'), ring: el.querySelector('.stk-ring'), fill: fill,
        bg: el.querySelector('.stk-bg'),
        circle: anime.svg.createDrawable(el.querySelector('.stk-circle')),
        tick: anime.svg.createDrawable(el.querySelector('.stk-tick')),
        timer: null, run: 0
      };
      anime.utils.set(s.bg, { opacity: fill ? 1 : 0.12 });
      if (reduced()) return;
      anime.utils.set(s.circle, { draw: '0 0' });
      anime.utils.set(s.tick, { draw: '0 0' });
      anime.utils.set(s.bg, { scale: 0 });
    }

    function play(el) {
      var s = el._stk, 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 });
      anime.utils.set(s.circle, { draw: '0 0' });
      anime.utils.set(s.tick, { draw: '0 0' });
      anime.utils.set(s.bg, { scale: 0 });
      anime.utils.set(s.ring, { opacity: 0, scale: 1 });

      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.6)' });
      anime.animate(s.tick, { draw: ['0 0', '0 1'], duration: dur * 0.6, delay: dur * 0.9, ease: 'out(3)' });
      // Pop as the tick lands, and send a ring outward.
      anime.animate(s.svg, {
        scale: [{ to: 1.1, duration: 160, ease: 'out(2)' }, { to: 1, duration: 600, ease: 'outElastic(1, .5)' }],
        delay: dur * 1.25
      });
      anime.animate(s.ring, { scale: [1, 1.55], opacity: [0.55, 0], duration: 750, delay: dur * 1.25, ease: 'out(3)' });

      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); }
        });
      }, dur * 1.25 + 600 + (+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-success-tick]').forEach(function (el) {
        if (el._stk) return;
        setup(el);
        if (!reduced()) io.observe(el);
      });
    }

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

<!-- filled, in your brand colour: -->
<div data-success-tick data-fill data-color="#6a0dad" aria-label="Payment sent"></div>