Add to Cart Fly
Press “Add to cart” and a copy of the product photo lifts off, arcs across the page shrinking as it goes, and drops into the cart icon. The cart bumps and its little badge counts up.
Trail Runner£89.00
How to use it
- Paste the snippet just before
</head>. - Add
data-add-to-cart-flyto the product card, withdata-fly-imageon its photo anddata-fly-buttonon its button. - Point
data-cartat your cart icon, or mark the icon withdata-fly-cart.
Options
Add any of these attributes to the same element.
data-cart | A CSS selector for the cart icon, e.g. #cart. Default the first [data-fly-cart]. |
data-duration | How long the flight takes. Default 900 ms. |
data-count | What the cart badge starts at. Default 0. |
data-autoplay | It adds to the cart by itself every few seconds while in view. Handy for demos. |
The cart gets a count badge unless it already holds a [data-fly-count] element. It fires a cart:add event with e.detail.count, and animacious.addToCartFly(card) plays it from code.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Add to Cart Fly: press "Add to cart" and a copy of the product image
// lifts off, arcs across the page shrinking as it goes, and drops into the
// cart icon, which bumps and counts up.
// Usage:
// <a href="/cart" id="cart">Cart</a> (anywhere on the page)
// <div data-add-to-cart-fly data-cart="#cart">
// <img data-fly-image src="shoe.jpg" alt="">
// <button data-fly-button>Add to cart</button>
// </div>
// The image is the element marked data-fly-image (or the first img), the
// button the one marked data-fly-button (or the first button). The cart is
// found with data-cart (a CSS selector), else the first [data-fly-cart]
// inside the element, else on the page.
// Optional: data-cart (selector), data-duration (ms, default 900),
// data-count (starting count for the cart badge, default 0), data-autoplay
// (it adds to the cart by itself every few seconds while in view; handy
// for demos).
// A small count badge is added to the cart unless it already holds a
// [data-fly-count] element. Listen for the "cart:add" event on the cart:
// e.detail.count is the new count.
// Script use: animacious.addToCartFly(el) plays it from code.
var DEFAULTS = { duration: 900, every: 2800 };
var css =
'.atc-ghost{position:fixed;z-index:2147483000;pointer-events:none;margin:0;border-radius:12px;overflow:hidden;' +
'box-shadow:0 12px 30px rgba(15,15,30,.25);will-change:transform}' +
'.atc-ghost>*{width:100%;height:100%;object-fit:cover}' +
'.atc-cart{position:relative}' +
'.atc-count{position:absolute;top:-.45em;right:-.55em;min-width:1.5em;height:1.5em;padding:0 .35em;border-radius:999px;' +
'background:#ef4444;color:#fff;font:700 11px/1.5em system-ui,sans-serif;text-align:center;box-sizing:border-box;pointer-events:none}' +
'.atc-count[data-zero]{transform:scale(0)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function findCart(el) {
var sel = el.dataset.cart;
return (sel && document.querySelector(sel)) || el.querySelector('[data-fly-cart]') || document.querySelector('[data-fly-cart]');
}
function setup(el) {
var cart = findCart(el);
var image = el.querySelector('[data-fly-image]') || el.querySelector('img');
var button = el.querySelector('[data-fly-button]') || el.querySelector('button');
if (!cart || !image) return;
if (getComputedStyle(cart).position === 'static') cart.classList.add('atc-cart');
var count = cart.querySelector('[data-fly-count]');
if (!count) {
count = document.createElement('span');
count.className = 'atc-count';
count.setAttribute('aria-live', 'polite');
cart.appendChild(count);
}
var n = +el.dataset.count || +count.textContent || 0;
count.textContent = n;
if (!n && count.classList.contains('atc-count')) count.setAttribute('data-zero', '');
el._atc = { cart: cart, image: image, count: count, n: n, timer: null };
if (button) {
button.addEventListener('click', function (e) {
if (e.isTrusted) stopAuto(el);
fly(el);
});
}
}
function bump(el) {
var s = el._atc;
s.n++;
s.count.textContent = s.n;
s.count.removeAttribute('data-zero');
s.cart.dispatchEvent(new CustomEvent('cart:add', { bubbles: true, detail: { count: s.n } }));
if (reduced()) return;
anime.animate(s.cart, { scale: [1, 1.22, 0.94, 1], rotate: [0, -10, 6, 0], duration: 600, ease: 'out(3)' });
anime.animate(s.count, { scale: [0.4, 1], duration: 500, ease: 'outElastic(1, .5)' });
}
function fly(el) {
var s = el._atc;
if (!s) return;
if (reduced()) { bump(el); return; }
var from = s.image.getBoundingClientRect();
var to = s.cart.getBoundingClientRect();
if (!from.width || !to.width) { bump(el); return; }
var ghost = document.createElement('div');
ghost.className = 'atc-ghost';
ghost.setAttribute('aria-hidden', 'true');
ghost.appendChild(s.image.cloneNode(true));
ghost.style.left = from.left + 'px';
ghost.style.top = from.top + 'px';
ghost.style.width = from.width + 'px';
ghost.style.height = from.height + 'px';
document.body.appendChild(ghost);
// A curved path from the image's centre to the cart's centre, bowing upwards.
var dx = to.left + to.width / 2 - (from.left + from.width / 2);
var dy = to.top + to.height / 2 - (from.top + from.height / 2);
var lift = Math.min(160, Math.abs(dx) * 0.35 + 50);
var cx = dx * 0.35, cy = Math.min(0, dy) - lift;
var endScale = Math.max(0.08, Math.min(to.width, to.height) / Math.max(from.width, from.height) * 0.7);
var p = { t: 0 };
anime.animate(ghost, { scale: [1, 1.08], duration: 160, ease: 'out(2)' });
anime.animate(p, {
t: 1,
delay: 120,
duration: +el.dataset.duration || DEFAULTS.duration,
ease: 'inOut(2)',
onUpdate: function () {
var t = p.t, u = 1 - t;
var x = 2 * u * t * cx + t * t * dx;
var y = 2 * u * t * cy + t * t * dy;
var sc = 1.08 + (endScale - 1.08) * t;
ghost.style.transform = 'translate(' + x + 'px,' + y + 'px) scale(' + sc + ') rotate(' + (t * 18) + 'deg)';
ghost.style.opacity = t > 0.85 ? (1 - t) / 0.15 : 1;
},
onComplete: function () {
ghost.remove();
bump(el);
}
});
}
function stopAuto(el) {
var s = el._atc;
if (s && s.timer) { clearInterval(s.timer); s.timer = null; s.stopped = true; }
}
function autoplay(el) {
var s = el._atc;
new IntersectionObserver(function (entries) {
clearInterval(s.timer);
s.timer = null;
if (!entries[0].isIntersecting || s.stopped) return;
setTimeout(function () { if (!s.stopped) fly(el); }, 300);
s.timer = setInterval(function () {
if (s.n >= 9) s.n = 0;
if (!el.matches(':hover')) fly(el);
}, DEFAULTS.every);
}, { threshold: 0.3 }).observe(el);
}
function init() {
document.querySelectorAll('[data-add-to-cart-fly]').forEach(function (el) {
if (el._atcDone) return;
el._atcDone = true;
setup(el);
if (el._atc && el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false' && !reduced()) autoplay(el);
});
}
window.animacious = window.animacious || {};
window.animacious.addToCartFly = function (el) { fly(el); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><a href="/cart" id="cart">Cart</a>
<div class="product" data-add-to-cart-fly data-cart="#cart">
<img data-fly-image src="trail-runner.jpg" alt="Trail Runner">
<h3>Trail Runner</h3>
<button data-fly-button>Add to cart</button>
</div>