Jello Wobble
A quick squash, then a skewing wobble that dies away, each sway half the last, like a jelly that has just been nudged. Ideal for pointing the eye at a button.
How to use it
- Paste the snippet just before
</head>. - Add
data-jello-wobbleto a button, link or heading.
Options
Add any of these attributes to the same element.
data-trigger | view (default) wobbles when scrolled into view. hover wobbles when the pointer arrives. |
data-loop | Wobble again every few seconds while on screen (with the view trigger). |
data-interval | Time between wobbles, when looping. Default 2600 ms. |
data-angle | The first, biggest skew, in degrees. Default 12. |
data-duration | Time for the whole wobble. Default 1000 ms. |
Use it sparingly: one wobbling call to action on a page is plenty.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Jello Wobble: the text skews one way, then the other, each wobble smaller
// than the last, like a jelly that has just been nudged. Good for drawing
// the eye to a button or a badge.
// Usage: <a class="btn" href="/signup" data-jello-wobble data-loop>Sign up</a>
// Optional: data-trigger ("view", the default, wobbles when scrolled into
// view; "hover" wobbles when the pointer arrives), data-loop (wobble every
// few seconds, with the view trigger), data-interval (ms between wobbles
// when looping; default 2600), data-angle (the first, biggest skew in
// degrees; default 12), data-duration (ms; default 1000).
var DEFAULTS = { interval: 2600, angle: 12, duration: 1000 };
var WOBBLES = 8; // each one half the size of the last, alternating sides
var css = '.jlw-box{will-change:transform}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// What moves: buttons, badges and other boxes move as a whole. A block
// heading's text goes in an inline box, so it moves around its own centre
// rather than the middle of the full-width block.
function build(el) {
var display = getComputedStyle(el).display;
if (display === 'inline') el.style.display = 'inline-block';
if (display !== 'block') { el.classList.add('jlw-box'); return el; }
var box = document.createElement('span');
box.className = 'jlw-box';
box.style.display = 'inline-block';
while (el.firstChild) box.appendChild(el.firstChild);
el.appendChild(box);
return box;
}
function wobble(el, box) {
var d = el.dataset;
var angle = +d.angle || DEFAULTS.angle;
var step = (+d.duration || DEFAULTS.duration) / (WOBBLES + 1);
function keys() {
var k = [];
for (var i = 0; i < WOBBLES; i++) {
k.push({ to: (i % 2 ? 1 : -1) * angle / Math.pow(2, i), duration: step, ease: 'inOutSine' });
}
k.push({ to: 0, duration: step, ease: 'outSine' });
return k;
}
return anime.animate(box, {
skewX: keys(),
skewY: keys(),
// A quick squash as it's hit, recovering over the first wobbles.
scaleY: [{ to: 0.92, duration: step, ease: 'out(2)' }, { to: 1, duration: step * 3, ease: 'outElastic(1, .5)' }]
});
}
function init() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var el = e.target;
el._jlwVisible = e.isIntersecting;
if (e.isIntersecting && !el._jlwPlayed) { el._jlwPlayed = true; el._jlwPlay(); }
});
}, { threshold: 0.5 });
document.querySelectorAll('[data-jello-wobble]').forEach(function (el) {
if (el._jlwPlay) return;
var d = el.dataset;
var box = build(el);
var busy = false;
el._jlwPlay = function () {
if (busy) return;
busy = true;
wobble(el, box).then(function () { busy = false; });
};
if (d.trigger === 'hover') {
el.addEventListener('pointerenter', el._jlwPlay);
return;
}
io.observe(el);
if (d.loop !== undefined && d.loop !== 'false') {
// Only while on screen.
setInterval(function () { if (el._jlwVisible) el._jlwPlay(); }, +d.interval || DEFAULTS.interval);
}
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><a class="btn" href="/signup" data-jello-wobble data-loop>Claim your spot</a>
<!-- wobble on hover instead: -->
<a class="btn" href="/signup" data-jello-wobble data-trigger="hover">Claim your spot</a>