← E-commerce

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

  1. Paste the snippet just before </head>.
  2. Add data-add-to-cart-fly to the product card, with data-fly-image on its photo and data-fly-button on its button.
  3. Point data-cart at your cart icon, or mark the icon with data-fly-cart.

Options

Add any of these attributes to the same element.

data-cartA CSS selector for the cart icon, e.g. #cart. Default the first [data-fly-cart].
data-durationHow long the flight takes. Default 900 ms.
data-countWhat the cart badge starts at. Default 0.
data-autoplayIt 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.

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