Idle Wiggle
Once the visitor has gone quiet for a few seconds (no moving, tapping, typing or scrolling), the element gives a small wiggle to remind them it’s there. It stays still while they’re busy.
Keep still for three seconds
How to use it
- Paste the snippet just before
</head>. - Add
data-idle-wiggleto any element. It wiggles whenever the visitor has been idle for a few seconds, and again every so often while they stay idle.
Options
Add any of these attributes to the same element.
data-idle | How long the visitor must be idle before it wiggles. Default 4000 ms. |
data-every | Time between wiggles while they stay idle. Default the same as data-idle. |
data-once | Wiggle only once each time they go idle. |
data-angle | How far it tilts, in degrees. Default 8. |
data-duration | How long the wiggle takes. Default 750 ms. |
data-watch | page counts activity anywhere on the page; self only counts the pointer over the element. Default page. |
It only wiggles while it’s on screen. Call animacious.idleWiggle(el) to wiggle it straight away from your own code.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Idle Wiggle: once the visitor has been inactive for a few seconds (no
// pointer movement, taps, keys or scrolling), the element gives a small
// wiggle to remind them it's there. It wiggles again every so often while
// they stay idle, and only while it's on screen.
// Usage: <button data-idle-wiggle>Need help?</button>
// Optional: data-idle (ms of inactivity before it wiggles, default 4000),
// data-every (ms between wiggles while still idle, default the same as
// data-idle), data-once (wiggle only once per idle spell), data-angle
// (degrees of the wiggle, default 8), data-duration (ms, default 750),
// data-watch ("page", default, counts activity anywhere on the page;
// "self" only counts the pointer over the element itself).
// Script use: animacious.idleWiggle(el) wiggles it now.
var DEFAULTS = { idle: 4000, angle: 8, duration: 750 };
var EVENTS = ['pointermove', 'pointerdown', 'keydown', 'wheel', 'scroll', 'touchstart'];
function on(v) { return v !== undefined && v !== 'false'; }
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function wiggle(el) {
var s = el._idw;
if (s.busy || reduced()) return;
s.busy = true;
var a = el.dataset.angle === undefined ? DEFAULTS.angle : +el.dataset.angle;
anime.animate(el, {
keyframes: [
{ rotate: -a, scale: 1.04 },
{ rotate: a },
{ rotate: -a * 0.6 },
{ rotate: a * 0.35 },
{ rotate: 0, scale: 1 }
],
duration: +el.dataset.duration || DEFAULTS.duration,
ease: 'inOut(2)',
onComplete: function () { s.busy = false; }
});
}
// Start (or restart) the countdown to the next wiggle.
function arm(el, wait) {
var s = el._idw;
clearTimeout(s.timer);
s.timer = setTimeout(function () {
if (s.visible) wiggle(el);
if (on(el.dataset.once)) return;
arm(el, +el.dataset.every || +el.dataset.idle || DEFAULTS.idle);
}, wait);
}
function active(el) {
arm(el, +el.dataset.idle || DEFAULTS.idle);
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { e.target._idw.visible = e.isIntersecting; });
}, { threshold: 0.2 });
document.querySelectorAll('[data-idle-wiggle]').forEach(function (el) {
if (el._idw) return;
el._idw = {};
if (reduced()) return;
io.observe(el);
var reset = function () { active(el); };
var target = el.dataset.watch === 'self' ? el : window;
EVENTS.forEach(function (t) { target.addEventListener(t, reset, { passive: true }); });
active(el);
});
}
window.animacious = window.animacious || {};
window.animacious.idleWiggle = function (el) { if (el._idw) wiggle(el); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button class="help" data-idle-wiggle>Need help?</button>
<!-- or after ten quiet seconds, just once: -->
<button class="help" data-idle-wiggle data-idle="10000" data-once>Need help?</button>