Cart Button Morph
Press “Add to cart” and the button squeezes into a quantity stepper, with minus and plus either side of the count. The number rolls up or down as it changes, and taking it back to zero stretches the stepper back out into the button.
Trail Runner£89.00
How to use it
- Paste the snippet just before
</head>. - Add
data-cart-button-morphto your “Add to cart” button. Size it withfont-sizeon its parent.
Options
Add any of these attributes to the same element.
data-max | The highest quantity. Default 9. |
data-color | Background colour. Default #6a0dad. |
data-autoplay | It adds, steps up and back down by itself while in view. Handy for demos. |
Each change fires a change event on the button with e.detail.quantity.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Cart Button Morph: press "Add to cart" and the button squeezes into a
// quantity stepper, with minus and plus either side of the count. The count
// rolls up or down as it changes; take it back to zero and the stepper
// stretches back out into the button.
// Usage: <button data-cart-button-morph>Add to cart</button>
// Size it with font-size on a parent.
// Optional: data-max (highest quantity, default 9), data-color (background,
// default #6a0dad), data-autoplay (it adds, steps up and back down by
// itself while in view; handy for demos).
// Listen for the "change" event on the button: e.detail.quantity.
var DEFAULTS = { max: 9, color: '#6a0dad', every: 950 };
var css =
'.cbm{position:relative;display:inline-block;vertical-align:middle;height:2.9em;border-radius:999px;overflow:hidden;' +
'background:var(--cbm-color);color:#fff;font-weight:600;box-shadow:0 8px 22px rgba(106,13,173,.25)}' +
'.cbm-add,.cbm-step{position:absolute;top:0;left:50%;height:100%;width:max-content;translate:-50% 0;display:flex;align-items:center;white-space:nowrap}' +
'.cbm-add{gap:.5em;padding:0 1.5em;margin:0;border:0;background:none;color:inherit;font:inherit;cursor:pointer}' +
'.cbm-add svg,.cbm-btn svg{width:1.15em;height:1.15em;flex:none}' +
'.cbm-step{gap:.25em;padding:0 .35em;visibility:hidden}' +
'.cbm-btn{display:grid;place-items:center;width:2.2em;height:2.2em;border-radius:50%;border:0;padding:0;margin:0;' +
'background:rgba(255,255,255,.18);color:inherit;font:inherit;cursor:pointer}' +
'.cbm-btn:hover{background:rgba(255,255,255,.3)}' +
'.cbm-num{position:relative;display:block;width:2.1em;height:1.3em;overflow:hidden;text-align:center;font-variant-numeric:tabular-nums}' +
'.cbm-num span{position:absolute;inset:0;line-height:1.3em}' +
'.cbm :focus-visible{outline:2px solid #fff;outline-offset:2px}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var CART = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4h2l2.4 11h11l2-8H6.2"/><circle cx="9.5" cy="19.5" r="1.4"/><circle cx="17" cy="19.5" r="1.4"/></svg>';
var MINUS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 12h12"/></svg>';
var PLUS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 12h12M12 6v12"/></svg>';
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
// In a card thumbnail (a link) there must be no real buttons: use spans.
var real = el.tagName === 'BUTTON';
var tag = real ? 'button' : 'span';
var wrap = document.createElement('span');
wrap.className = 'cbm';
wrap.style.setProperty('--cbm-color', d.color || DEFAULTS.color);
el.parentNode.insertBefore(wrap, el);
wrap.appendChild(el);
el.classList.add('cbm-add');
if (real && !el.getAttribute('type')) el.type = 'button';
if (!el.querySelector('svg')) el.insertAdjacentHTML('afterbegin', CART);
var step = document.createElement('span');
step.className = 'cbm-step';
if (real) { step.setAttribute('role', 'group'); step.setAttribute('aria-label', 'Quantity'); }
step.innerHTML =
'<' + tag + ' class="cbm-btn" data-cbm="-1"' + (real ? ' type="button" aria-label="Remove one"' : '') + '>' + MINUS + '</' + tag + '>' +
'<span class="cbm-num"' + (real ? ' aria-live="polite"' : '') + '><span>1</span></span>' +
'<' + tag + ' class="cbm-btn" data-cbm="1"' + (real ? ' type="button" aria-label="Add one"' : '') + '>' + PLUS + '</' + tag + '>';
wrap.appendChild(step);
var s = el._cbm = { wrap: wrap, add: el, step: step, num: step.querySelector('.cbm-num'), q: 0, max: +d.max || DEFAULTS.max };
wrap.style.width = el.offsetWidth + 'px';
el.addEventListener('click', function (e) { if (e.isTrusted) s.user = true; set(el, 1); });
step.addEventListener('click', function (e) {
var b = e.target.closest('[data-cbm]');
if (!b) return;
if (e.isTrusted) s.user = true;
set(el, s.q + +b.getAttribute('data-cbm'));
});
new ResizeObserver(function () {
wrap.style.width = (s.q ? step : el).offsetWidth + 'px';
}).observe(el);
}
function rollNumber(s, from, to) {
var old = s.num.firstChild;
var next = document.createElement('span');
next.textContent = to;
s.num.appendChild(next);
if (reduced()) { old.remove(); return; }
var dir = to > from ? 1 : -1;
anime.animate(old, { y: [0, -dir * 100 + '%'], opacity: [1, 0], duration: 300, ease: 'in(2)', onComplete: function () { old.remove(); } });
anime.animate(next, { y: [dir * 100 + '%', 0], opacity: [0, 1], duration: 420, ease: 'outBack(1.6)' });
}
function set(el, q) {
var s = el._cbm;
q = Math.max(0, Math.min(s.max, q));
if (q === s.q) {
// Already at the limit: a little shake says no.
if (q === s.max && !reduced()) anime.animate(s.num, { x: [0, -4, 4, -2, 0], duration: 300 });
return;
}
var was = s.q;
s.q = q;
el.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { quantity: q } }));
var morph = !was || !q;
if (!morph) { rollNumber(s, was, q); return; }
var show = q ? s.step : s.add, hide = q ? s.add : s.step;
if (q) { s.num.innerHTML = '<span>' + q + '</span>'; }
show.style.visibility = 'visible';
if (show === s.step) s.add.setAttribute('aria-hidden', 'true'); else s.add.removeAttribute('aria-hidden');
var w = show.offsetWidth;
if (reduced()) {
s.wrap.style.width = w + 'px';
hide.style.visibility = 'hidden';
return;
}
var focus = document.activeElement && s.wrap.contains(document.activeElement);
anime.animate(s.wrap, { width: w, duration: 520, ease: 'outElastic(1, .75)' });
anime.animate(s.wrap, { scaleY: [1, 0.88, 1], duration: 360, ease: 'out(2)' });
anime.animate(hide, { opacity: [1, 0], scale: [1, 0.7], duration: 180, ease: 'in(2)', onComplete: function () { hide.style.visibility = 'hidden'; } });
anime.animate(show, { opacity: [0, 1], scale: [0.7, 1], duration: 380, delay: 120, ease: 'outBack(1.8)' });
if (q && s.step.children.length) {
anime.animate(s.step.children[0], { x: [16, 0], duration: 460, delay: 140, ease: 'outBack(2)' });
anime.animate(s.step.children[2], { x: [-16, 0], duration: 460, delay: 140, ease: 'outBack(2)' });
}
if (focus) (q ? s.step.querySelector('[data-cbm="1"]') : s.add).focus();
}
function autoplay(el) {
var s = el._cbm, timer = null, i = 0;
var script = [1, 2, 3, 2, 1, 0, 0];
new IntersectionObserver(function (entries) {
clearInterval(timer);
if (!entries[0].isIntersecting) return;
timer = setInterval(function () {
if (s.user) { clearInterval(timer); return; }
if (s.wrap.matches(':hover')) return;
set(el, script[i++ % script.length]);
}, DEFAULTS.every);
}, { threshold: 0.3 }).observe(s.wrap);
}
function init() {
document.querySelectorAll('[data-cart-button-morph]').forEach(function (el) {
if (el._cbm) return;
setup(el);
if (el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false' && !reduced()) autoplay(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button data-cart-button-morph>Add to cart</button>
<script>
document.querySelector('[data-cart-button-morph]')
.addEventListener('change', (e) => updateBasket(e.detail.quantity));
</script>