Swing Drop In
The element drops down as if hung on a string, then swings like a pendulum from a point above it, each swing smaller, until it hangs at rest. A looping demo swings the other way first each time.
Open
How to use it
- Paste the snippet just before
</head>. - Add
data-swing-drop-into any element. - Set
data-stringto how far above it the string is tied, if you draw one.
Options
Add any of these attributes to the same element.
data-angle | How far it swings at first, in degrees. Default 24. |
data-string | How far above the element’s top it hangs from, in px. A longer string makes a wider swing. Default 120. |
data-height | Drop this many px instead of from just above the top of its container. |
data-duration | How long the drop and swings take. Default 1800 ms. |
data-delay | Wait before the first entrance. Default 0 ms. |
data-loop | Hold, fade away and drop in again. |
data-hold | How long it stays before fading, when looping. Default 1400 ms. |
It drops from just above the top of the nearest container that clips its overflow (overflow: hidden or clip), or of the window. It swings round a point above itself, so the script sets transform-origin on the element.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Swing Drop In: the element drops down as if hung on a string, then
// swings like a pendulum from a point above it, each swing smaller,
// until it hangs at rest.
// Usage: <div data-swing-drop-in>…</div>
// It drops from just above the top of the nearest container that clips
// its overflow (overflow: hidden or clip), or of the window. The script
// sets transform-origin on the element (the point it hangs from).
// Optional: data-angle (how far it swings at first, in degrees, default
// 24), data-string (how far above the element it hangs from, in px,
// default 120), data-height (px to drop instead), data-duration (ms,
// default 1800), data-delay (ms before the first entrance), data-loop
// (hold, fade away and drop in again; each loop swings the other way
// first), data-hold (ms to stay when looping, default 1400).
var DEFAULTS = { angle: 24, string: 120, duration: 1800, delay: 0, hold: 1400 };
function reduced() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// The box the element drops into.
function clipBox(el) {
for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
var s = getComputedStyle(p);
if (s.overflowX !== 'visible' || s.overflowY !== 'visible') return p.getBoundingClientRect();
}
return { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
}
function enter(el, dir, done) {
var d = el.dataset;
var t = +d.duration || DEFAULTS.duration;
var a = (d.angle === undefined ? DEFAULTS.angle : +d.angle || 0) * dir;
var len = d.string === undefined ? DEFAULTS.string : +d.string || 0;
anime.utils.set(el, { y: 0, rotate: 0 });
var h = +d.height || el.getBoundingClientRect().bottom - clipBox(el).top + 8;
// Hang from a point above the element's top centre.
el.style.transformOrigin = '50% ' + -len + 'px';
anime.utils.set(el, { y: -h, rotate: a * 0.5, opacity: 1 });
anime.createTimeline({ onComplete: done })
// Drop until the string pulls tight, with a little give at the end.
.add(el, {
y: [
{ to: 6, duration: t * 0.22, ease: 'in(2)' },
{ to: 0, duration: t * 0.1, ease: 'out(2)' }
]
}, 0)
// Swing back and forth, each swing smaller, and come to rest.
.add(el, {
rotate: [
{ to: a, duration: t * 0.2, ease: 'out(2)' },
{ to: -a * 0.62, duration: t * 0.18, ease: 'inOut(2)' },
{ to: a * 0.36, duration: t * 0.16, ease: 'inOut(2)' },
{ to: -a * 0.18, duration: t * 0.15, ease: 'inOut(2)' },
{ to: a * 0.07, duration: t * 0.14, ease: 'inOut(2)' },
{ to: 0, duration: t * 0.17, ease: 'out(2)' }
]
}, 0);
}
function cycle(el) {
var d = el.dataset;
var s = el._sdi;
enter(el, s.n++ % 2 ? -1 : 1, function () {
if (d.loop === undefined || d.loop === 'false') return;
setTimeout(function () {
anime.animate(el, {
opacity: 0,
duration: 400,
ease: 'out(2)',
onComplete: function () {
// Wait until it's back on screen before the next entrance.
setTimeout(function () {
if (s.visible) cycle(el);
else s.waiting = true;
}, 500);
}
});
}, +d.hold || DEFAULTS.hold);
});
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._sdi;
s.visible = e.isIntersecting;
if (!e.isIntersecting) return;
if (!s.started) {
s.started = true;
setTimeout(function () { cycle(e.target); }, +e.target.dataset.delay || DEFAULTS.delay);
} else if (s.waiting) {
s.waiting = false;
cycle(e.target);
}
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-swing-drop-in]').forEach(function (el) {
if (el._sdi) return;
el._sdi = { n: 0 };
if (reduced()) return;
anime.utils.set(el, { opacity: 0 });
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div class="sign" data-swing-drop-in>Open</div>
<!-- or a gentler swing from a shorter string: -->
<div class="sign" data-swing-drop-in data-angle="12" data-string="60">Open</div>