Bouncing Arrow Pointer
An arrow appears beside the element and keeps bouncing towards it, tapping at it like a friendly “over here”. It can sit on any side, and get out of the way once the pointer arrives.
Only 12 places left
How to use it
- Paste the snippet just before
</head>. - Add
data-bouncing-arrow-pointerto any element. The arrow appears beside it and bounces while it’s on screen.
Options
Add any of these attributes to the same element.
data-side | Where the arrow sits: left, right, top or bottom. Default left. |
data-size | How big the arrow is, in px. Default 28. |
data-gap | Space between the arrow and the element, in px. Default 10. |
data-distance | How far each bounce travels, in px. Default 10. |
data-duration | How long one bounce takes. Default 700 ms. |
data-color | The arrow’s colour. Default #6a0dad. |
data-hover | Set to hide to fade the arrow out while the pointer is over the element or it has focus. |
The arrow is added inside the element and hangs off its side, so don’t clip the element’s overflow, and leave room for it. Call animacious.arrowPointer(el, false) to remove it, for example once it’s been clicked, and animacious.arrowPointer(el) to bring it back.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Bouncing Arrow Pointer: an arrow appears beside the element and keeps
// bouncing towards it, pointing at it, like a "click here" hint. It pauses
// while off screen.
// Usage: <button data-bouncing-arrow-pointer>Sign up</button>
// Optional: data-side (where the arrow sits: "left", default, "right",
// "top" or "bottom"), data-size (px, default 28), data-gap (px between the
// arrow and the element, default 10), data-distance (px each bounce
// travels, default 10), data-duration (ms per bounce, default 700),
// data-color (default #6a0dad), data-hover="hide" (hide the arrow while the
// pointer is over the element or it has focus).
// The arrow is added inside the element, so don't clip its overflow.
// Script use: animacious.arrowPointer(el, false) removes the arrow (for
// example once it has been clicked); animacious.arrowPointer(el) brings it back.
var DEFAULTS = { side: 'left', size: 28, gap: 10, distance: 10, duration: 700, color: '#6a0dad' };
var css =
'.bap-arrow{position:absolute;z-index:2;pointer-events:none;line-height:0;transition:opacity .25s}' +
'.bap-arrow svg{display:block;width:100%;height:100%;overflow:visible;fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}' +
'.bap-off{opacity: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 side = d.side || DEFAULTS.side;
var size = +d.size || DEFAULTS.size;
var gap = d.gap === undefined ? DEFAULTS.gap : +d.gap;
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
var a = document.createElement('span');
a.className = 'bap-arrow';
a.setAttribute('aria-hidden', 'true');
a.style.width = a.style.height = size + 'px';
a.style.color = d.color || DEFAULTS.color;
var turn = { left: 0, top: 90, right: 180, bottom: -90 }[side] || 0;
a.innerHTML = '<svg viewBox="0 0 24 24" style="transform:rotate(' + turn + 'deg)"><path d="M3 12h16M13 6l6 6-6 6"/></svg>';
var half = -size / 2 + 'px';
if (side === 'right') { a.style.left = '100%'; a.style.top = '50%'; a.style.marginTop = half; a.style.marginLeft = gap + 'px'; }
else if (side === 'top') { a.style.bottom = '100%'; a.style.left = '50%'; a.style.marginLeft = half; a.style.marginBottom = gap + 'px'; }
else if (side === 'bottom') { a.style.top = '100%'; a.style.left = '50%'; a.style.marginLeft = half; a.style.marginTop = gap + 'px'; }
else { a.style.right = '100%'; a.style.top = '50%'; a.style.marginTop = half; a.style.marginRight = gap + 'px'; }
el.appendChild(a);
var s = (el._bap = { arrow: a });
if (reduced()) return;
// Towards the element is +x for an arrow on the left, -y for one below, and so on.
var prop = side === 'top' || side === 'bottom' ? 'y' : 'x';
var sign = side === 'right' || side === 'bottom' ? -1 : 1;
var dist = d.distance === undefined ? DEFAULTS.distance : +d.distance;
var dur = +d.duration || DEFAULTS.duration;
var back = {}, toward = {};
back[prop] = -sign * dist;
toward[prop] = 0;
// Pulled back slowly, then it darts in and taps the element.
back.duration = dur * 0.6;
back.ease = 'out(2)';
toward.duration = dur * 0.4;
toward.ease = 'in(3)';
s.anim = anime.createTimeline({ loop: true, autoplay: false }).add(a, back).add(a, toward);
if (d.hover === 'hide') {
var hide = function () { a.classList.add('bap-off'); };
var show = function () { if (!s.removed) a.classList.remove('bap-off'); };
el.addEventListener('pointerenter', hide);
el.addEventListener('pointerleave', show);
el.addEventListener('focusin', hide);
el.addEventListener('focusout', show);
}
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._bap;
if (!s.anim) return;
if (e.isIntersecting && !s.removed) s.anim.play();
else s.anim.pause();
});
}, { threshold: 0 });
document.querySelectorAll('[data-bouncing-arrow-pointer]').forEach(function (el) {
if (el._bap) return;
setup(el);
io.observe(el);
});
}
window.animacious = window.animacious || {};
window.animacious.arrowPointer = function (el, show) {
var s = el._bap;
if (!s) return;
s.removed = show === false;
s.arrow.classList.toggle('bap-off', s.removed);
if (s.anim) { if (s.removed) s.anim.pause(); else s.anim.play(); }
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button data-bouncing-arrow-pointer>Claim your spot</button>
<!-- or a red arrow underneath, pointing up: -->
<button data-bouncing-arrow-pointer data-side="bottom" data-color="#dc2626">Claim your spot</button>