Arrow Slide Button
Hover the button and its label slips out to the side as an arrow slides in to take its place. Or, with data-mode="nudge", the label just steps aside to make room for an arrow beside it. The button keeps its size throughout.
How to use it
- Paste the snippet just before
</head>. - Add
data-arrow-slide-buttonto a button or link.
Options
Add any of these attributes to the same element.
data-mode | swap (the arrow replaces the label) or nudge (the label steps aside for it). Default swap. |
data-duration | How long the slide takes. Default 420 ms. |
data-autoplay | It slides by itself every couple of seconds while in view. Handy for demos. |
Keyboard focus sets it off too, and on touch screens a tap shows the arrow for a moment. The arrow uses the text colour.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Arrow Slide Button: hover the button and its label slides aside as an
// arrow slides in. By default the label slips out to the right and the
// arrow takes its place in the middle; with data-mode="nudge" the label
// just steps left to make room for an arrow beside it. Keyboard focus and
// taps set it off too.
// Usage: <button data-arrow-slide-button>Continue</button>
// Optional: data-mode ("swap", the default, or "nudge"), data-duration (ms,
// default 420), data-autoplay (it slides by itself every couple of seconds
// while in view; handy for demos).
// The button keeps its size throughout. With reduced motion it swaps
// instantly.
var DEFAULTS = { duration: 420, every: 2600 };
var ARROW = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M4 12h15M13 6l6 6-6 6"/></svg>';
var css =
'.asb{position:relative;overflow:hidden}' +
'.asb-label{display:inline-block}' +
'.asb-arrow{position:absolute;top:50%;left:50%;width:1.25em;height:1.25em;margin:-.625em 0 0 -.625em;opacity:0;pointer-events:none}' +
'.asb-arrow svg{display:block;width:100%;height:100%}' +
'.asb-nudge .asb-arrow{left:auto;right:var(--asb-pad);margin-left:0}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var nudge = d.mode === 'nudge';
var duration = reduced() ? 0 : +d.duration || DEFAULTS.duration;
el.classList.add('asb');
if (nudge) el.classList.add('asb-nudge');
var label = document.createElement('span');
label.className = 'asb-label';
while (el.firstChild) label.appendChild(el.firstChild);
el.appendChild(label);
var arrow = document.createElement('span');
arrow.className = 'asb-arrow';
arrow.setAttribute('aria-hidden', 'true');
arrow.innerHTML = ARROW;
el.appendChild(arrow);
el.style.setProperty('--asb-pad', parseFloat(getComputedStyle(el).paddingRight) * 0.55 + 'px');
var out = false;
function slide(on) {
if (on === out) return;
out = on;
var w = el.offsetWidth, a = arrow.offsetWidth;
var ease = on ? 'out(3)' : 'inOut(2)';
if (nudge) {
anime.animate(label, { x: on ? -a * 0.6 : 0, duration: duration, ease: ease });
anime.animate(arrow, {
x: on ? [-a, 0] : -a * 0.6, opacity: on ? 1 : 0,
duration: duration, delay: on ? duration * 0.15 : 0, ease: ease
});
} else {
// Label exits right, arrow arrives from the left; reversed on the way back.
anime.animate(label, {
x: on ? w * 0.55 : [-w * 0.55, 0], opacity: on ? 0 : [0, 1],
duration: duration, delay: on ? 0 : duration * 0.15, ease: ease
});
anime.animate(arrow, {
x: on ? [-w * 0.5, 0] : w * 0.5, opacity: on ? 1 : 0,
duration: duration, delay: on ? duration * 0.15 : 0, ease: ease
});
}
}
function busy() { return el.matches(':hover') || document.activeElement === el; }
el.addEventListener('pointerenter', function (e) { if (e.pointerType !== 'touch') slide(true); });
el.addEventListener('pointerleave', function (e) { if (e.pointerType !== 'touch' && document.activeElement !== el) slide(false); });
el.addEventListener('focus', function () { slide(true); });
el.addEventListener('blur', function () { if (!el.matches(':hover')) slide(false); });
var touchTimer = null;
el.addEventListener('pointerdown', function (e) {
if (e.pointerType !== 'touch') return;
clearTimeout(touchTimer);
slide(true);
touchTimer = setTimeout(function () { slide(false); }, 1200);
});
if (!reduced() && d.autoplay !== undefined && d.autoplay !== 'false') {
var timer = null;
var pass = function () {
if (!busy()) {
slide(true);
setTimeout(function () { if (!busy()) slide(false); }, DEFAULTS.every * 0.5);
}
timer = setTimeout(pass, DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(pass, 500);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-arrow-slide-button]').forEach(function (el) {
if (el._asb) return;
el._asb = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><a class="button" href="/checkout" data-arrow-slide-button>Continue</a>
<!-- the label steps aside instead: -->
<a class="button" href="/step-2" data-arrow-slide-button data-mode="nudge">Next step</a>