← Attention

Edit-mode Jiggle

The element jiggles on the spot like an app icon waiting to be rearranged. Each one gets its own slightly different rhythm, so a whole grid looks natural rather than marching in step.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-edit-mode-jiggle to each element. They jiggle while they’re on screen.
  3. Or add data-press too, to start them still: a long press on one starts all of them in the same parent, and a tap anywhere else stops them.

Options

Add any of these attributes to the same element.

data-angleHow far it tilts each way, in degrees. Default 1.6.
data-speedHow long one jiggle takes. Default 260 ms.
data-badgeShow a round “remove” badge on the top-left corner while it jiggles.
data-pressStart still, and jiggle after a long press, as above.

Call animacious.jiggle(el) to start it from your own code and animacious.jiggle(el, false) to stop it, for example from an Edit / Done button. With reduced motion it stays still but still shows its badge.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Edit-mode Jiggle: the element jiggles on the spot, like an app icon
    // waiting to be rearranged. Each element gets its own slightly different
    // rhythm, so a grid of them looks natural. It pauses while off screen.
    // Usage: <span class="icon" data-edit-mode-jiggle>…</span>
    // Optional: data-angle (degrees each way, default 1.6), data-speed (ms per
    // jiggle, default 260), data-badge (show a round "remove" badge on the
    // top-left corner while it jiggles), data-press (start still: a long press
    // starts every jiggler in the same parent, a tap outside them stops them).
    // Script use: animacious.jiggle(el) starts it, animacious.jiggle(el, false)
    // stops it.
    var DEFAULTS = { angle: 1.6, speed: 260 };

    var css =
      '@keyframes emj-a{0%,100%{transform:rotate(calc(var(--emj-a) * -1))}50%{transform:rotate(var(--emj-a))}}' +
      '@keyframes emj-b{0%,100%{transform:rotate(var(--emj-a)) translateY(0)}25%{transform:rotate(0deg) translateY(-.6px)}' +
      '50%{transform:rotate(calc(var(--emj-a) * -1)) translateY(0)}75%{transform:rotate(0deg) translateY(.6px)}}' +
      '.emj-on{animation:emj-a var(--emj-s) ease-in-out infinite;animation-delay:var(--emj-d)}' +
      '.emj-on.emj-alt{animation-name:emj-b}' +
      '.emj-paused{animation-play-state:paused}' +
      '.emj-badge{position:absolute;left:-7px;top:-7px;z-index:2;display:grid;place-items:center;width:22px;height:22px;' +
      'border-radius:50%;background:rgba(60,60,67,.82);box-shadow:0 1px 3px rgba(0,0,0,.25);pointer-events:none;' +
      'transform:scale(0);transition:transform .25s cubic-bezier(.3,1.6,.5,1)}' +
      '.emj-badge::before{content:"";width:10px;height:2px;border-radius:1px;background:#fff}' +
      '.emj-on>.emj-badge,.emj-still>.emj-badge{transform:scale(1)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function on(v) { return v !== undefined && v !== 'false'; }
    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    var count = 0;

    function setup(el) {
      var d = el.dataset;
      var speed = +d.speed || DEFAULTS.speed;
      var angle = d.angle === undefined ? DEFAULTS.angle : +d.angle;
      // Every element gets a slightly different speed, start point and pattern.
      el.style.setProperty('--emj-a', angle + 'deg');
      el.style.setProperty('--emj-s', Math.round(speed * (0.9 + Math.random() * 0.2)) + 'ms');
      el.style.setProperty('--emj-d', -Math.round(Math.random() * speed) + 'ms');
      if (count++ % 2) el.classList.add('emj-alt');
      if (on(d.badge)) {
        if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
        var b = document.createElement('span');
        b.className = 'emj-badge';
        b.setAttribute('aria-hidden', 'true');
        el.appendChild(b);
      }
      el._emj = { active: false };
    }

    function jiggle(el, yes) {
      var s = el._emj;
      if (!s) return;
      s.active = yes !== false;
      // With reduced motion it stays still, but still shows the badge.
      el.classList.toggle(reduced() ? 'emj-still' : 'emj-on', s.active);
    }

    function siblings(el) {
      return Array.prototype.filter.call(el.parentElement.children, function (c) { return c._emj; });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          e.target.classList.toggle('emj-paused', !e.isIntersecting);
        });
      }, { threshold: 0 });
      var pressable = [];
      document.querySelectorAll('[data-edit-mode-jiggle]').forEach(function (el) {
        if (el._emj) return;
        setup(el);
        io.observe(el);
        if (!on(el.dataset.press)) { jiggle(el); return; }
        pressable.push(el);
        var timer;
        el.addEventListener('pointerdown', function () {
          clearTimeout(timer);
          timer = setTimeout(function () { siblings(el).forEach(function (c) { jiggle(c); }); }, 500);
        });
        ['pointerup', 'pointerleave', 'pointercancel'].forEach(function (t) {
          el.addEventListener(t, function () { clearTimeout(timer); });
        });
        el.addEventListener('contextmenu', function (e) { e.preventDefault(); });
      });
      if (pressable.length && !init.bound) {
        init.bound = true;
        // A tap anywhere outside the jigglers ends edit mode.
        document.addEventListener('pointerdown', function (e) {
          if (e.target.closest && e.target.closest('[data-edit-mode-jiggle]')) return;
          document.querySelectorAll('[data-edit-mode-jiggle][data-press]').forEach(function (el) { jiggle(el, false); });
        });
      }
    }

    window.animacious = window.animacious || {};
    window.animacious.jiggle = jiggle;
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div class="home-screen">
  <span class="app" data-edit-mode-jiggle data-badge>…</span>
  <span class="app" data-edit-mode-jiggle data-badge>…</span>
</div>

<!-- or only after a long press: -->
<span class="app" data-edit-mode-jiggle data-badge data-press>…</span>