Map Pin Drop
Pins fall onto your map one after another. Each lands with a squash and a little bounce, its shadow firming up beneath it, and a ripple spreads out across the map from where it hit. Then its label pops up beside it.
Bakery
Park café
Bookshop
Gallery
Market
How to use it
- Paste the snippet just before
</head>. - Add
data-map-pin-dropto a container. Put the map first (an<img>or inline SVG), then one child per pin. - Place each pin’s point with
data-xanddata-y, from 0 to 100 across and down the map. Its text, if any, becomes its label.
Options
Add any of these attributes to the same element.
data-x / data-y | On a pin: where its point lands, 0–100 from the top left. |
data-color | Pin colour, on the container or on a single pin. Default #7c3aed. |
data-stagger | Gap between pins. Default 380 ms. |
data-loop | Lift the pins off and drop them again, in a new order. |
Labels sit to the right of their pin, or to the left near the right-hand edge. Pins and labels are sized in em, so set font-size on the container to scale them.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Map Pin Drop: pins fall onto a map one after another, land with a
// squash and a bounce, and send a ripple out across the map. Their labels
// pop up beside them.
// Usage:
// <div data-map-pin-drop>
// <img src="map.png" alt="Map of the town centre">
// <span data-x="30" data-y="40">Café</span>
// <span data-x="72" data-y="65"></span>
// </div>
// The picture comes first, then one child per pin. data-x and data-y place
// the pin's point, 0-100 across and down the picture. Its text (if any) is
// the label.
// Optional: data-color (pin colour, default #7c3aed; also on a single
// pin), data-stagger (ms between pins, default 380), data-loop (lift the
// pins off and drop them again).
var DEFAULTS = { color: '#7c3aed', stagger: 380 };
var css =
'.mpd{position:relative;display:block;line-height:0;overflow:hidden;border-radius:.8em}' +
'.mpd>img,.mpd>svg{display:block;width:100%;height:auto}' +
'.mpd-spot{position:absolute;width:0;height:0;line-height:1.2}' +
'.mpd-pin{position:absolute;left:-.8em;bottom:0;width:1.6em;height:2.2em;transform-origin:50% 100%;opacity:0;filter:drop-shadow(0 .12em .1em rgba(30,10,60,.25))}' +
'.mpd-pin svg{display:block;width:100%;height:100%;overflow:visible}' +
'.mpd-shadow{position:absolute;left:-.55em;top:-.18em;width:1.1em;height:.36em;border-radius:50%;background:rgba(30,10,60,.3);opacity:0}' +
'.mpd-ripple{position:absolute;left:-1em;top:-1em;width:2em;height:2em;border-radius:50%;border:.14em solid var(--mpd-c);box-sizing:border-box;opacity:0}' +
'.mpd-label{position:absolute;left:1em;bottom:1.2em;white-space:nowrap;padding:.25em .6em;border-radius:.5em;background:#fff;color:#1f1530;font-weight:700;font-size:.85em;box-shadow:0 .2em .7em rgba(30,10,60,.18);opacity:0;transform-origin:0 100%}' +
'.mpd-spot.mpd-left .mpd-label{left:auto;right:1em;transform-origin:100% 100%}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function pinSvg(color) {
return '<svg viewBox="0 0 32 44" aria-hidden="true"><path d="M16 43C16 43 2 26.5 2 16a14 14 0 0 1 28 0c0 10.5-14 27-14 27z" fill="' + color +
'" stroke="#fff" stroke-width="2"/><circle cx="16" cy="16" r="5.5" fill="#fff"/></svg>';
}
function setup(el) {
var d = el.dataset;
var s = (el._mpd = { pins: [] });
el.classList.add('mpd');
Array.from(el.children).slice(1).forEach(function (k) {
var color = k.getAttribute('data-color') || d.color || DEFAULTS.color;
var x = parseFloat(k.getAttribute('data-x')), y = parseFloat(k.getAttribute('data-y'));
var text = k.textContent.trim();
var spot = document.createElement('div');
spot.className = 'mpd-spot' + (x > 62 ? ' mpd-left' : '');
spot.style.left = (isNaN(x) ? 50 : x) + '%';
spot.style.top = (isNaN(y) ? 50 : y) + '%';
spot.style.setProperty('--mpd-c', color);
spot.innerHTML = '<span class="mpd-ripple" aria-hidden="true"></span><span class="mpd-shadow" aria-hidden="true"></span><span class="mpd-pin">' + pinSvg(color) + '</span>';
if (text) {
var label = document.createElement('span');
label.className = 'mpd-label';
label.textContent = text;
spot.appendChild(label);
}
el.replaceChild(spot, k);
s.pins.push({
spot: spot, pin: spot.querySelector('.mpd-pin'), shadow: spot.querySelector('.mpd-shadow'),
ripple: spot.querySelector('.mpd-ripple'), label: spot.querySelector('.mpd-label'),
});
});
}
function drop(el, p, delay) {
var fall = Math.max(60, p.spot.offsetTop + 40);
anime.utils.set(p.pin, { opacity: 1, y: -fall, scaleX: 1, scaleY: 1 });
anime.utils.set(p.shadow, { y: 0 });
var tl = anime.createTimeline({ delay: delay });
tl.add(p.pin, { y: [-fall, 0], duration: 480, ease: 'in(3)' }, 0)
.add(p.shadow, { opacity: [0, 1], scale: [0.2, 1], duration: 480, ease: 'in(2)' }, 0)
.add(p.pin, { scaleY: [1, 0.72, 1.08, 1], scaleX: [1, 1.2, 0.95, 1], duration: 520, ease: 'out(2)' }, 480)
.add(p.pin, { y: [0, -14, 0, -4, 0], duration: 620, ease: 'out(2)' }, 520)
.add(p.shadow, { scale: [1, 0.7, 1, 0.9, 1], duration: 620, ease: 'out(2)' }, 520)
.add(p.ripple, { scale: [0.2, 2.6], opacity: [0.9, 0], duration: 900, ease: 'out(3)' }, 470)
.add(p.ripple, { scale: [0.2, 2], opacity: [0.6, 0], duration: 900, ease: 'out(3)' }, 700);
if (p.label) tl.add(p.label, { opacity: [0, 1], scale: [0.6, 1], y: [6, 0], duration: 420, ease: 'outBack(2)' }, 760);
return tl;
}
function play(el) {
var s = el._mpd, stagger = +el.dataset.stagger || DEFAULTS.stagger;
var order = s.pins.slice();
if (s.ran) order.sort(function () { return Math.random() - 0.5; });
s.ran = true;
order.forEach(function (p, i) { drop(el, p, 250 + i * stagger); });
if (el.dataset.loop === undefined) return;
var total = 250 + order.length * stagger + 2600;
setTimeout(function () {
s.pins.forEach(function (p, i) {
var parts = [p.pin, p.shadow].concat(p.label ? [p.label] : []);
anime.animate(parts, { opacity: 0, y: '-=12', duration: 360, delay: i * 60, ease: 'in(2)' });
});
setTimeout(function () { play(el); }, 360 + s.pins.length * 60 + 300);
}, total);
}
function still(el) {
el._mpd.pins.forEach(function (p) {
p.pin.style.opacity = 1;
p.shadow.style.opacity = 1;
if (p.label) p.label.style.opacity = 1;
});
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
play(e.target);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-map-pin-drop]').forEach(function (el) {
if (el._mpd) return;
setup(el);
if (reduced()) still(el);
else io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-map-pin-drop style="max-width: 600px">
<img src="map.png" alt="Map of the town centre">
<span data-x="27" data-y="28">Bakery</span>
<span data-x="44" data-y="62" data-color="#db2777">Bookshop</span>
<span data-x="82" data-y="74">Gallery</span>
</div>