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
- Paste the snippet just before
</head>. - Add an empty
<div data-success-tick></div>where the tick should appear. Itsfont-sizeis the size of the icon. - 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-color | Colour of the circle and tick. Default #16a34a. |
data-fill | Fill the circle in behind a white tick. |
data-duration | How long the circle takes to draw. The tick follows in proportion. Default 650 ms. |
data-loop | Fade away after a pause and draw again. |
data-hold | How 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.
<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><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>