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
- Paste the snippet just before
</head>. - Add
data-order-truck-buttonto your order button.
Options
Add any of these attributes to the same element.
data-done | The text once the truck has gone. Default “Order placed”. |
data-color | Truck colour. Default white. |
data-reset | How long it waits before going back to the start. By default it stays done. |
data-loop | Play again every few seconds while in view. |
data-autoplay | It 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.
<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><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>