← Attention

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

  1. Paste the snippet just before </head>.
  2. Add data-idle-wiggle to 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-idleHow long the visitor must be idle before it wiggles. Default 4000 ms.
data-everyTime between wiggles while they stay idle. Default the same as data-idle.
data-onceWiggle only once each time they go idle.
data-angleHow far it tilts, in degrees. Default 8.
data-durationHow long the wiggle takes. Default 750 ms.
data-watchpage 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.

1 · Snippet
<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>
2 · Your HTML
<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>