← E-commerce

Order Truck Button

Press “Place order” and the label slips away, a little delivery truck rolls in, a parcel drops into the back, and off it drives along a dashed road. Then the button confirms the order with a tick.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-order-truck-button to your order button.

Options

Add any of these attributes to the same element.

data-doneThe text once the truck has gone. Default “Order placed”.
data-colorTruck colour. Default white.
data-resetHow long it waits before going back to the start. By default it stays done.
data-loopPlay again every few seconds while in view.
data-autoplayIt presses itself every few seconds while in view, until someone presses it. Handy for demos.

It fires an order event when pressed, so you can submit the order then.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Order Truck Button: press "Place order" and the label slips away, a
    // little delivery truck rolls in, a parcel drops into the back, and the
    // truck drives off along a dashed road. Then the button confirms the order
    // with a tick.
    // Usage: <button data-order-truck-button>Place order</button>
    // Optional: data-done (text once it has gone, default "Order placed"),
    // data-color (truck colour, default #ffffff), data-loop (reset and play
    // again every few seconds), data-reset (ms before it goes back to the
    // start after playing; default stays done, or 2200 with data-loop or
    // data-autoplay), data-autoplay (it presses itself while in view; handy
    // for demos).
    // Listen for the "order" event on the button when it is pressed.
    var DEFAULTS = { done: 'Order placed', color: '#ffffff', reset: 2200 };

    var css =
      '.otb{position:relative;display:inline-grid;place-items:center;min-width:12em;height:3.4em;padding:0 1.6em;border:0;border-radius:999px;overflow:hidden;' +
      'background:#1a1a2e;color:#fff;font:inherit;font-weight:600;cursor:pointer;-webkit-tap-highlight-color:transparent;isolation:isolate}' +
      '.otb-label,.otb-done{grid-area:1/1;display:flex;align-items:center;gap:.45em;white-space:nowrap}' +
      '.otb-done{opacity:0}' +
      '.otb-done svg{width:1.1em;height:1.1em}' +
      '.otb-scene{position:absolute;inset:0;pointer-events:none;opacity:0}' +
      '.otb-road{position:absolute;left:0;right:0;bottom:.55em;height:2px;' +
      'background:repeating-linear-gradient(90deg,rgba(255,255,255,.5) 0 .7em,transparent .7em 1.3em);background-size:1.3em 2px}' +
      '.otb-truck{position:absolute;left:50%;bottom:.62em;width:3.2em;height:1.9em;margin-left:-1.6em}' +
      '.otb-truck svg{display:block;width:100%;height:100%;overflow:visible}' +
      '.otb-box{position:absolute;left:50%;bottom:1.32em;width:1.2em;height:1em;margin-left:-1.25em;border-radius:3px;background:#f2b766;' +
      'box-shadow:inset 0 -.12em 0 rgba(0,0,0,.15)}' +
      '.otb-box::after{content:"";position:absolute;left:42%;top:0;bottom:0;width:16%;background:rgba(0,0,0,.18)}' +
      '.otb-wheel{transform-box:fill-box;transform-origin:center}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var TICK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';

    function truckSvg(color) {
      // Cargo box at the back (left), cab at the front (right).
      return '<svg viewBox="0 0 64 38" aria-hidden="true">' +
        '<path d="M2 12h38v16H2z" fill="' + color + '"/>' +
        '<path d="M42 11h10.5c1.6 0 3 .8 3.8 2.1L61 21v7H42z" fill="' + color + '"/>' +
        '<path d="M46 14h6.2l3.6 6H46z" fill="#1a1a2e" opacity=".8"/>' +
        '<rect x="2" y="26" width="59" height="3" rx="1.5" fill="' + color + '" opacity=".75"/>' +
        '<g class="otb-wheel"><circle cx="14" cy="31" r="5.5" fill="#1a1a2e" stroke="' + color + '" stroke-width="2.5"/><path d="M14 27.5v7" stroke="' + color + '" stroke-width="1.5"/></g>' +
        '<g class="otb-wheel"><circle cx="50" cy="31" r="5.5" fill="#1a1a2e" stroke="' + color + '" stroke-width="2.5"/><path d="M50 27.5v7" stroke="' + color + '" stroke-width="1.5"/></g>' +
        '</svg>';
    }

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function on(v) { return v !== undefined && v !== 'false'; }

    function setup(el) {
      var d = el.dataset;
      el.classList.add('otb');
      if (el.tagName === 'BUTTON' && !el.getAttribute('type')) el.type = 'button';
      var text = el.textContent.trim() || 'Place order';
      el.textContent = '';
      var label = document.createElement('span');
      label.className = 'otb-label';
      label.textContent = text;
      var done = document.createElement('span');
      done.className = 'otb-done';
      done.setAttribute('aria-hidden', 'true');
      done.innerHTML = TICK + '<span></span>';
      done.lastChild.textContent = d.done || DEFAULTS.done;
      var scene = document.createElement('span');
      scene.className = 'otb-scene';
      scene.setAttribute('aria-hidden', 'true');
      scene.innerHTML = '<span class="otb-road"></span><span class="otb-box"></span><span class="otb-truck">' + truckSvg(d.color || DEFAULTS.color) + '</span>';
      el.appendChild(label);
      el.appendChild(done);
      el.appendChild(scene);
      el._otb = {
        label: label, done: done, scene: scene, road: scene.children[0], box: scene.children[1], truck: scene.children[2],
        wheels: scene.querySelectorAll('.otb-wheel'), busy: false, placed: false
      };
      el.addEventListener('click', function (e) {
        if (e.isTrusted) el._otbUser = true;
        play(el);
      });
    }

    function reset(el) {
      var s = el._otb;
      s.placed = false;
      s.done.setAttribute('aria-hidden', 'true');
      s.label.removeAttribute('aria-hidden');
      anime.animate(s.done, { opacity: 0, y: '-.6em', duration: 220, ease: 'in(2)' });
      anime.animate(s.label, { opacity: [0, 1], y: ['.6em', 0], duration: 380, delay: 160, ease: 'out(3)' });
    }

    function play(el) {
      var s = el._otb;
      if (s.busy || s.placed) return;
      s.busy = true;
      el.setAttribute('aria-busy', 'true');
      el.dispatchEvent(new CustomEvent('order', { bubbles: true }));
      var d = el.dataset;
      var hold = +d.reset || (on(d.loop) || on(d.autoplay) ? DEFAULTS.reset : 0);
      var finish = function () {
        s.busy = false;
        s.placed = true;
        el.removeAttribute('aria-busy');
        if (hold) setTimeout(function () { reset(el); }, hold);
      };
      if (reduced()) {
        anime.utils.set(s.label, { opacity: 0 });
        anime.utils.set(s.done, { opacity: 1, y: 0 });
        s.label.setAttribute('aria-hidden', 'true');
        s.done.removeAttribute('aria-hidden');
        finish();
        return;
      }
      var w = el.offsetWidth, em = parseFloat(getComputedStyle(el).fontSize);
      var off = w / 2 + 3 * em; // far enough to be fully out of the button
      anime.utils.set(s.truck, { x: -off, y: 0, rotate: 0 });
      anime.utils.set(s.box, { x: 0, y: -2.4 * em, opacity: 1, rotate: -20 });
      anime.utils.set(s.scene, { opacity: 1 });

      var tl = anime.createTimeline({ onComplete: finish });
      tl.add(s.label, { opacity: [1, 0], y: [0, '-.8em'], duration: 260, ease: 'in(2)' }, 0)
        .add(s.road, { opacity: [0, 1], duration: 300 }, 100)
        .add(s.truck, { x: [-off, 0], duration: 620, ease: 'out(3)' }, 160)
        .add(s.wheels, { rotate: [0, 360], duration: 620, ease: 'out(3)' }, 160)
        // The parcel drops into the back and the truck squats under it.
        .add(s.box, { y: [-2.4 * em, 0], rotate: [-20, 0], duration: 420, ease: 'outBounce' }, 820)
        .add(s.truck, { y: [0, '.12em', 0], duration: 300, ease: 'out(2)' }, 1060)
        // A little reverse to wind up, then off it goes.
        .add([s.truck, s.box], { x: '-=' + (0.35 * em), duration: 220, ease: 'out(2)' }, 1420)
        .add([s.truck, s.box], { x: off + 0.4 * em, duration: 720, ease: 'in(3)' }, 1640)
        .add(s.wheels, { rotate: '-=' + 60, duration: 220, ease: 'out(2)' }, 1420)
        .add(s.wheels, { rotate: '+=' + 720, duration: 720, ease: 'in(3)' }, 1640)
        .add(s.road, { backgroundPositionX: ['0em', '-6.5em'], duration: 720, ease: 'in(3)' }, 1640)
        .add(s.scene, { opacity: 0, duration: 200 }, 2300)
        .add(s.done, { opacity: [0, 1], y: ['.8em', 0], duration: 420, ease: 'outBack(1.8)' }, 2250)
        .call(function () {
          s.label.setAttribute('aria-hidden', 'true');
          s.done.removeAttribute('aria-hidden');
        }, 2250);
    }

    function autoplay(el) {
      var timer = null;
      new IntersectionObserver(function (entries) {
        clearInterval(timer);
        if (!entries[0].isIntersecting) return;
        var tick = function () { if (!el._otbUser && !el.matches(':hover')) play(el); };
        setTimeout(tick, 500);
        timer = setInterval(tick, 5600);
      }, { threshold: 0.3 }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-order-truck-button]').forEach(function (el) {
        if (el._otb) return;
        setup(el);
        var d = el.dataset;
        if ((on(d.autoplay) || on(d.loop)) && !reduced()) autoplay(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<button data-order-truck-button>Place order</button>

<!-- different wording, back to the start after 3 s: -->
<button data-order-truck-button data-done="On its way!" data-reset="3000">Checkout</button>